diff --git a/CMakeLists.txt b/CMakeLists.txt index 967f1ccd7..efc4cbdd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,11 @@ if (BOOST_REDIS_MAIN_PROJECT) json endian compat + url + filesystem + scope + capy + corosio ) foreach(dep IN LISTS deps) @@ -90,6 +95,9 @@ if (BOOST_REDIS_MAIN_PROJECT) INTERFACE Boost::system Boost::asio + Boost::capy + Boost::corosio + Boost::corosio_openssl Threads::Threads OpenSSL::Crypto OpenSSL::SSL @@ -118,6 +126,9 @@ else() Boost::mp11 Boost::system Boost::throw_exception + Boost::capy + Boost::corosio + Boost::corosio_openssl Threads::Threads OpenSSL::Crypto OpenSSL::SSL @@ -130,7 +141,7 @@ if (BOOST_REDIS_MAIN_PROJECT) endif() # Most tests require a running Redis server, so we only run them if we're the main project -if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) +# if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) # Tests and common utilities add_subdirectory(test) @@ -139,4 +150,4 @@ if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) # Examples add_subdirectory(example) -endif() +# endif() diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index e40a7dae4..ddb56601f 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(benchmarks_options INTERFACE) -target_link_libraries(benchmarks_options INTERFACE boost_redis_src) +target_link_libraries(benchmarks_options INTERFACE boost_redis_asio) target_link_libraries(benchmarks_options INTERFACE boost_redis_project_options) target_compile_features(benchmarks_options INTERFACE cxx_std_20) diff --git a/doc/modules/ROOT/pages/cancellation.adoc b/doc/modules/ROOT/pages/cancellation.adoc index 78ed20076..33f3fba4a 100644 --- a/doc/modules/ROOT/pages/cancellation.adoc +++ b/doc/modules/ROOT/pages/cancellation.adoc @@ -52,7 +52,7 @@ error after sending the request, but before receiving a response? In this situation there is no way to know if the request was processed by the server or not. By default, the library will consider the request as failed, -and `async_exec` will complete with an `asio::error::operation_aborted` +and `async_exec` will complete with an `capy::error::canceled` error code. Some requests can be executed several times and result in the same outcome diff --git a/doc/modules/ROOT/pages/pushes.adoc b/doc/modules/ROOT/pages/pushes.adoc index cae1187d6..974d738e4 100644 --- a/doc/modules/ROOT/pages/pushes.adoc +++ b/doc/modules/ROOT/pages/pushes.adoc @@ -15,6 +15,12 @@ The most common case is https://redis.io/docs/latest/develop/pubsub/[Pub/Sub messages] triggered by `PUBLISH`. The following example shows a typical receiver: +[tabs] +======== +Asio:: ++ +-- + [source,cpp] ---- auto receiver(std::shared_ptr conn) -> asio::awaitable @@ -62,6 +68,67 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable } } ---- +-- + +Corosio:: ++ +-- + +[source,cpp] +---- +capy::io_task<> receiver(co_connection& conn) +{ + generic_flat_response resp; + conn.set_receive_response(resp); + + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + auto [sub_ec] = co_await conn.exec(req); + if (sub_ec) { + std::cerr << "Error subscribing: " << sub_ec << std::endl; + co_return {}; + } + + // You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored + // in resp. If the connection encounters a network error and reconnects to the server, + // it will automatically subscribe to 'mychannel' again. This is transparent to the user. + // You need to use the specialized request::subscribe() function (instead of request::push) + // to enable this behavior. + + // Loop to read Redis push messages. The loop terminates when receive() reports an error + // (e.g. cancellation when the surrounding when_any cascade tears down the run loop). + while (true) { + // Wait for pushes + auto [ec] = co_await conn.receive(); + + // Check for errors and cancellations + if (ec) { + std::cerr << "Error during receive: " << ec << std::endl; + co_return {}; + } + + // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) + if (resp.has_error()) { + std::cerr << "The receive response contains an error: " << resp.error().diagnostic + << std::endl; + co_return {}; + } + + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (push_view elem : push_parser(resp.value())) { + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload + << "\n"; + } + + resp.value().clear(); + } +} +---- +-- + +======== Summary of the steps: diff --git a/doc/modules/ROOT/pages/reference.adoc b/doc/modules/ROOT/pages/reference.adoc index 085ea3cbc..404e57a2d 100644 --- a/doc/modules/ROOT/pages/reference.adoc +++ b/doc/modules/ROOT/pages/reference.adoc @@ -23,6 +23,8 @@ xref:reference:boost/redis/connection.adoc[`connection`] xref:reference:boost/redis/basic_connection.adoc[`basic_connection`] +xref:reference:boost/redis/co_connection.adoc[`co_connection`] + xref:reference:boost/redis/address.adoc[`address`] xref:reference:boost/redis/role.adoc[`role`] diff --git a/doc/mrdocs.cpp b/doc/mrdocs.cpp index 1543a509c..eb52412d7 100644 --- a/doc/mrdocs.cpp +++ b/doc/mrdocs.cpp @@ -8,3 +8,4 @@ #define BOOST_ALLOW_DEPRECATED // avoid mrdocs errors with the BOOST_DEPRECATED macro #include +#include diff --git a/doc/package-lock.json b/doc/package-lock.json index e4d516316..3a6b0c3f8 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@asciidoctor/tabs": "^1.0.0-beta.6", "@cppalliance/antora-cpp-reference-extension": "^0.1.0", "antora": "^3.1.10" } @@ -310,6 +311,15 @@ "yarn": ">=1.1.0" } }, + "node_modules/@asciidoctor/tabs": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@asciidoctor/tabs/-/tabs-1.0.0-beta.6.tgz", + "integrity": "sha512-gGZnW7UfRXnbiyKNd9PpGKtSuD8+DsqaaTSbQ1dHVkZ76NaolLhdQg8RW6/xqN3pX1vWZEcF4e81+Oe9rNRWxg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@cppalliance/antora-cpp-reference-extension": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@cppalliance/antora-cpp-reference-extension/-/antora-cpp-reference-extension-0.1.0.tgz", diff --git a/doc/package.json b/doc/package.json index 4384079fc..480239e79 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@asciidoctor/tabs": "^1.0.0-beta.6", "@cppalliance/antora-cpp-reference-extension": "^0.1.0", "antora": "^3.1.10" } diff --git a/doc/redis-playbook.yml b/doc/redis-playbook.yml index 54a901e81..7400a02c3 100644 --- a/doc/redis-playbook.yml +++ b/doc/redis-playbook.yml @@ -25,6 +25,8 @@ asciidoc: attributes: # Scrolling problems appear without this page-pagination: '' + extensions: + - '@asciidoctor/tabs' content: sources: diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 55fde8579..575d4fbaf 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,10 +1,8 @@ add_library(examples_main STATIC main.cpp) -target_link_libraries(examples_main PRIVATE boost_redis_project_options) +target_link_libraries(examples_main PUBLIC boost_redis_asio) function(make_example EXAMPLE_NAME) add_executable(${EXAMPLE_NAME} ${EXAMPLE_NAME}.cpp) - target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_src) - target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_project_options) if (ARGN) target_link_libraries(${EXAMPLE_NAME} PRIVATE ${ARGN}) endif() @@ -15,20 +13,30 @@ function(make_testable_example EXAMPLE_NAME) add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379) endfunction() -make_testable_example(cpp17_intro) -make_testable_example(cpp17_intro_sync) +make_testable_example(cpp17_intro boost_redis_asio) +make_testable_example(cpp17_intro_sync boost_redis_asio) make_testable_example(cpp20_intro examples_main) +make_testable_example(corosio_intro boost_redis_corosio) make_testable_example(cpp20_containers examples_main) +make_testable_example(corosio_containers boost_redis_corosio) make_testable_example(cpp20_json examples_main Boost::json Boost::container_hash) +make_testable_example(corosio_json boost_redis_corosio Boost::json Boost::container_hash) make_testable_example(cpp20_unix_sockets examples_main) +make_testable_example(corosio_unix_sockets boost_redis_corosio) make_testable_example(cpp20_timeouts examples_main) +make_testable_example(corosio_timeouts boost_redis_corosio) make_testable_example(cpp20_sentinel examples_main) +make_testable_example(corosio_sentinel boost_redis_corosio) -make_example(cpp20_subscriber examples_main) -make_example(cpp20_streams examples_main) -make_example(cpp20_echo_server examples_main) -make_example(cpp20_intro_tls examples_main) +make_example(cpp20_subscriber examples_main) +make_example(corosio_subscriber boost_redis_corosio) +make_example(cpp20_streams examples_main) +make_example(corosio_streams boost_redis_corosio) +make_example(cpp20_echo_server examples_main) +make_example(corosio_echo_server boost_redis_corosio) +make_example(cpp20_intro_tls examples_main) +make_example(corosio_intro_tls boost_redis_corosio) # We test the protobuf example only on gcc. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -48,7 +56,8 @@ endif() # We build and test the spdlog integration example only if the library is found find_package(spdlog) if (spdlog_FOUND) - make_testable_example(cpp17_spdlog spdlog::spdlog) + make_testable_example(cpp17_spdlog spdlog::spdlog) + make_testable_example(corosio_spdlog spdlog::spdlog boost_redis_corosio) else() message(STATUS "Skipping the spdlog example because the spdlog package couldn't be found") endif() diff --git a/example/corosio_containers.cpp b/example/corosio_containers.cpp new file mode 100644 index 000000000..ba33a4280 --- /dev/null +++ b/example/corosio_containers.cpp @@ -0,0 +1,193 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +template +std::ostream& operator<<(std::ostream& os, std::optional const& opt) +{ + if (opt.has_value()) + std::cout << opt.value(); + else + std::cout << "null"; + + return os; +} + +void print(std::map const& cont) +{ + for (auto const& e : cont) + std::cout << e.first << ": " << e.second << "\n"; +} + +template +void print(std::vector const& cont) +{ + for (auto const& e : cont) + std::cout << e << " "; + std::cout << "\n"; +} + +// Stores the content of some STL containers in Redis. +capy::task<> store(co_connection& conn) +{ + std::vector vec{1, 2, 3, 4, 5, 6}; + + std::map map{ + {"key1", "value1"}, + {"key2", "value2"}, + {"key3", "value3"} + }; + + request req; + req.push_range("RPUSH", "rpush-key", vec); + req.push_range("HSET", "hset-key", map); + req.push("SET", "key", "value"); + + auto [ec] = co_await conn.exec(req, ignore); + if (ec) { + std::cerr << "Error in store: " << ec << std::endl; + exit(1); + } +} + +capy::task<> hgetall(co_connection& conn) +{ + // A request contains multiple commands. + request req; + req.push("HGETALL", "hset-key"); + + // Responses as tuple elements. + response> resp; + + // Executes the request and reads the response. + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in hgetall: " << ec << std::endl; + exit(1); + } + + print(std::get<0>(resp).value()); +} + +capy::task<> mget(co_connection& conn) +{ + // A request contains multiple commands. + request req; + req.push("MGET", "key", "non-existing-key"); + + // Responses as tuple elements. + response>> resp; + + // Executes the request and reads the response. + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in mget: " << ec << std::endl; + exit(1); + } + + print(std::get<0>(resp).value()); +} + +// Retrieves in a transaction. +capy::task<> transaction(co_connection& conn) +{ + request req; + req.push("MULTI"); + req.push("LRANGE", "rpush-key", 0, -1); // Retrieves + req.push("HGETALL", "hset-key"); // Retrieves + req.push("MGET", "key", "non-existing-key"); + req.push("EXEC"); + + response< + ignore_t, // multi + ignore_t, // lrange + ignore_t, // hgetall + ignore_t, // mget + response< + std::optional>, + std::optional>, + std::optional>>> // exec + > + resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in transaction: " << ec << std::endl; + exit(1); + } + + print(std::get<0>(std::get<4>(resp).value()).value().value()); + print(std::get<1>(std::get<4>(resp).value()).value().value()); + print(std::get<2>(std::get<4>(resp).value()).value().value()); +} + +capy::task co_main(config cfg) +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection in parallel with the work coroutine. + // when_any will cancel run() once the work completes. + co_await capy::when_any( + [&]() -> capy::io_task<> { + co_await store(conn); + co_await transaction(conn); + co_await hgetall(conn); + co_await mget(conn); + co_return {}; + }(), + conn.run(cfg)); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main(config{})); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_echo_server.cpp b/example/corosio_echo_server.cpp new file mode 100644 index 000000000..66c136221 --- /dev/null +++ b/example/corosio_echo_server.cpp @@ -0,0 +1,140 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +// Echoes lines received over TCP back to the sender, going through Redis (PING). +capy::io_task<> echo_server_session(corosio::tcp_socket socket, co_connection& conn) +{ + request req; + response resp; + + for (std::string buffer;;) { + auto [read_ec, n] = co_await capy::read_until( + socket, + capy::string_dynamic_buffer(&buffer), + "\n"); + if (read_ec) { + std::cerr << "Error reading from session socket: " << read_ec << std::endl; + co_return {}; + } + + req.push("PING", buffer); + auto [exec_ec] = co_await conn.exec(req, resp); + if (exec_ec) { + std::cerr << "Error executing PING: " << exec_ec << std::endl; + co_return {}; + } + + auto const& reply = std::get<0>(resp).value(); + auto [write_ec, written] = co_await capy::write( + socket, + capy::make_buffer(reply.data(), reply.size())); + if (write_ec) { + std::cerr << "Error writing to session socket: " << write_ec << std::endl; + co_return {}; + } + + std::get<0>(resp).value().clear(); + req.clear(); + buffer.erase(0, n); + } +} + +// Listens for tcp connections. +capy::io_task<> listener(co_connection& conn) +{ + auto ex = co_await capy::this_coro::executor; + corosio::tcp_acceptor acc{ex, corosio::endpoint(static_cast(55555))}; + + for (;;) { + corosio::tcp_socket peer{ex}; + auto [ec] = co_await acc.accept(peer); + if (ec) { + std::clog << "Listener: " << ec.message() << std::endl; + co_return {}; + } + + // Spawn the session as a detached task on the same executor. + // The new task runs independently and does not inherit the listener's stop token. + capy::run_async(ex)(echo_server_session(std::move(peer), conn)); + } +} + +// Wait for SIGINT or SIGTERM; completing this task triggers a graceful shutdown +// by cancelling the surviving siblings via the when_any cascade. +capy::io_task<> signal_handler() +{ + corosio::signal_set signals{(co_await capy::this_coro::executor).context(), SIGINT, SIGTERM}; + auto [ec, signum] = co_await signals.wait(); + if (!ec) + std::cout << "Received signal " << signum << ", shutting down\n"; + co_return {}; +}; + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection, the listener and the signal waiter in parallel. + // when_any will cancel the surviving tasks once any one of them completes. + co_await capy::when_any(listener(conn), conn.run(config{}), signal_handler()); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_intro.cpp b/example/corosio_intro.cpp new file mode 100644 index 000000000..05488356a --- /dev/null +++ b/example/corosio_intro.cpp @@ -0,0 +1,76 @@ +/* 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 + +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +namespace corosio = boost::corosio; + +capy::io_task<> run_request(co_connection& conn) +{ + // A request containing only a ping command. + request req; + req.push("PING", "Hello world"); + + // Response where the PONG response will be stored. + response resp; + + // Executes the request. + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cout << "Error executing PING: " << ec << std::endl; + } else { + std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; + } + + co_return {}; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the PING request, in parallel + co_await capy::when_any(run_request(conn), conn.run(config{})); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_intro_tls.cpp b/example/corosio_intro_tls.cpp new file mode 100644 index 000000000..c563a3fda --- /dev/null +++ b/example/corosio_intro_tls.cpp @@ -0,0 +1,94 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include + +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +capy::io_task<> run_request(co_connection& conn) +{ + request req; + req.push("PING"); + + response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cout << "Error executing PING: " << ec << std::endl; + } else { + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; + } + + co_return {}; +} + +capy::task co_main() +{ + // Configure a TLS connection to the public test server + config cfg; + cfg.use_ssl = true; + cfg.username = "aedis"; + cfg.password = "aedis"; + cfg.addr.host = "db.occase.de"; + cfg.addr.port = "6380"; + + // Configure the TLS context + corosio::tls_context tls_ctx; + if (auto ec = tls_ctx.set_verify_mode(corosio::tls_verify_mode::require_peer)) { + std::cerr << "Error in set_verify_mode: " << ec << std::endl; + exit(1); + } + tls_ctx.set_hostname("db.occase.de"); + + // Create a connection using the configured TLS context + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; + + // Run the connection and the PING request, in parallel + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_json.cpp b/example/corosio_json.cpp new file mode 100644 index 000000000..4b9c7322b --- /dev/null +++ b/example/corosio_json.cpp @@ -0,0 +1,116 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +namespace resp3 = boost::redis::resp3; +using namespace boost::describe; +using namespace boost::redis; +using boost::redis::resp3::node_view; + +// Struct that will be stored in Redis using json serialization. +struct user { + std::string name; + std::string age; + std::string country; +}; + +// The type must be described for serialization to work. +BOOST_DESCRIBE_STRUCT(user, (), (name, age, country)) + +// Boost.Redis customization points (example/json.hpp) +void boost_redis_to_bulk(std::string& to, user const& u) +{ + resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u))); +} + +void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&) +{ + u = boost::json::value_to(boost::json::parse(node.value)); +} + +capy::io_task<> run_request(co_connection& 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("SET", "json-key", u); // Stores in Redis. + req.push("GET", "json-key"); // Retrieves from Redis. + + response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing request: " << ec << std::endl; + exit(1); + } + + std::cout << "Name: " << std::get<1>(resp).value().name << "\n" + << "Age: " << std::get<1>(resp).value().age << "\n" + << "Country: " << std::get<1>(resp).value().country << "\n"; + + co_return {}; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the JSON request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(config{})); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_sentinel.cpp b/example/corosio_sentinel.cpp new file mode 100644 index 000000000..80218b225 --- /dev/null +++ b/example/corosio_sentinel.cpp @@ -0,0 +1,98 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +capy::io_task<> run_request(co_connection& conn) +{ + // You can use the connection normally, as you would use a connection to a single master. + request req; + req.push("PING", "Hello world"); + response resp; + + // Execute the request. + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } + + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; + co_return {}; +} + +capy::task co_main() +{ + // Boost.Redis has built-in support for Sentinel deployments. + // To enable it, set the fields in config shown here. + // sentinel.addresses should contain a list of (hostname, port) pairs + // where Sentinels are listening. IPs can also be used. + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + + // Set master_name to the identifier that you configured + // in the "sentinel monitor" statement of your sentinel.conf file + cfg.sentinel.master_name = "mymaster"; + + // run() will contact the Sentinels, obtain the master address, + // connect to it and keep the connection healthy. If a failover happens, + // the address will be resolved again and the new elected master will be contacted. + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_spdlog.cpp b/example/corosio_spdlog.cpp new file mode 100644 index 000000000..080ced3f8 --- /dev/null +++ b/example/corosio_spdlog.cpp @@ -0,0 +1,127 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +namespace redis = boost::redis; + +// Maps a Boost.Redis log level to a spdlog log level +static spdlog::level::level_enum to_spdlog_level(redis::logger::level lvl) +{ + switch (lvl) { + // spdlog doesn't include the emerg and alert syslog levels, + // so we convert them to the highest supported level. + // Similarly, notice is similar to info + case redis::logger::level::emerg: + case redis::logger::level::alert: + case redis::logger::level::crit: return spdlog::level::critical; + case redis::logger::level::err: return spdlog::level::err; + case redis::logger::level::warning: return spdlog::level::warn; + case redis::logger::level::notice: + case redis::logger::level::info: return spdlog::level::info; + case redis::logger::level::debug: + default: return spdlog::level::debug; + } +} + +// This function glues Boost.Redis logging and spdlog. +// It should have the signature shown here. It will be invoked +// by Boost.Redis whenever a message is to be logged. +static void do_log(redis::logger::level level, std::string_view msg) +{ + spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg); +} + +capy::io_task<> run_request(redis::co_connection& conn) +{ + // Execute a request + redis::request req; + req.push("PING", "Hello world"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + spdlog::error("Request failed: {}", ec.message()); + exit(1); + } + + spdlog::info("PING: {}", std::get<0>(resp).value()); + co_return {}; +} + +capy::task co_main(redis::config cfg) +{ + // Create a connection to connect to Redis, and pass it a custom logger. + // Boost.Redis will call do_log whenever it needs to log a message. + // Note that the function will only be called for messages with level >= info + // (i.e. filtering is done by Boost.Redis). + redis::co_connection conn{ + co_await capy::this_coro::executor, + redis::logger{redis::logger::level::info, do_log} + }; + + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} + +int main(int argc, char** argv) +{ + try { + // Configuration to connect to the server. Adjust as required + redis::config cfg; + if (argc == 3) { + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; + } + + // Create an execution context, required to create any I/O objects + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + spdlog::error("Error: {}", e.what()); + } + exit(1); + })(co_main(cfg)); + + // Actually run our example. Nothing will happen until we call run() + ctx.run(); + + } catch (std::exception const& e) { + spdlog::error("Error: {}", e.what()); + return 1; + } +} diff --git a/example/corosio_streams.cpp b/example/corosio_streams.cpp new file mode 100644 index 000000000..1cef143f8 --- /dev/null +++ b/example/corosio_streams.cpp @@ -0,0 +1,97 @@ +/* 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 + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include +#include +#include +#include + +namespace net = boost::asio; +using boost::redis::config; +using boost::redis::generic_response; +using boost::redis::operation; +using boost::redis::request; +using boost::redis::connection; +using net::signal_set; + +auto stream_reader(std::shared_ptr conn) -> net::awaitable +{ + std::string redisStreamKey_; + request req; + generic_response resp; + + std::string stream_id{"$"}; + std::string const field = "myfield"; + + for (;;) { + req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id); + co_await conn->async_exec(req, resp); + + //std::cout << "Response: "; + //for (auto i = 0UL; i < resp->size(); ++i) { + // std::cout << resp->at(i).value << ", "; + //} + //std::cout << std::endl; + + // The following approach was taken in order to be able to + // deal with the responses, as generated by redis in the case + // that there are multiple stream 'records' within a single + // generic_response. The nesting and number of values in + // resp.value() are different, depending on the contents + // of the stream in redis. Uncomment the above commented-out + // code for examples while running the XADD command. + + std::size_t item_index = 0; + while (item_index < std::size(resp.value())) { + auto const& val = resp.value().at(item_index).value; + + if (field.compare(val) == 0) { + // We've hit a myfield field. + // The streamId is located at item_index - 2 + // The payload is located at item_index + 1 + stream_id = resp.value().at(item_index - 2).value; + std::cout << "StreamId: " << stream_id << ", " + << "MyField: " << resp.value().at(item_index + 1).value << std::endl; + ++item_index; // We can increase so we don't read this again + } + + ++item_index; + } + + req.clear(); + resp.value().clear(); + } +} + +// Run this in another terminal: +// redis-cli -r 100000 -i 0.0001 XADD "test-topic" "*" "myfield" "myfieldvalue1" +auto co_main(config cfg) -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(ex); + net::co_spawn(ex, stream_reader(conn), net::detached); + + // Disable health checks. + cfg.health_check_interval = std::chrono::seconds::zero(); + 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/example/corosio_subscriber.cpp b/example/corosio_subscriber.cpp new file mode 100644 index 000000000..c0f96f9c9 --- /dev/null +++ b/example/corosio_subscriber.cpp @@ -0,0 +1,128 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include + +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +/* This example will subscribe and read pushes indefinitely. + * + * To test send messages with redis-cli + * + * $ redis-cli -3 + * 127.0.0.1:6379> PUBLISH mychannel some-message + * (integer) 3 + * 127.0.0.1:6379> + * + * To test reconnection try, for example, to close all clients currently + * connected to the Redis instance + * + * $ redis-cli + * > CLIENT kill TYPE pubsub + */ + +// Receives server pushes. +capy::io_task<> receiver(co_connection& conn) +{ + generic_flat_response resp; + conn.set_receive_response(resp); + + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + auto [sub_ec] = co_await conn.exec(req); + if (sub_ec) { + std::cerr << "Error subscribing: " << sub_ec << std::endl; + co_return {}; + } + + // You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored + // in resp. If the connection encounters a network error and reconnects to the server, + // it will automatically subscribe to 'mychannel' again. This is transparent to the user. + // You need to use the specialized request::subscribe() function (instead of request::push) + // to enable this behavior. + + // Loop to read Redis push messages. The loop terminates when receive() reports an error + // (e.g. cancellation when the surrounding when_any cascade tears down the run loop). + while (true) { + // Wait for pushes + auto [ec] = co_await conn.receive(); + + // Check for errors and cancellations + if (ec) { + std::cerr << "Error during receive: " << ec << std::endl; + co_return {}; + } + + // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) + if (resp.has_error()) { + std::cerr << "The receive response contains an error: " << resp.error().diagnostic + << std::endl; + co_return {}; + } + + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (push_view elem : push_parser(resp.value())) { + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload + << "\n"; + } + + resp.value().clear(); + } +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the receiver loop in parallel. + // when_any will cancel the surviving task once the other completes. + co_await capy::when_any(receiver(conn), conn.run(config{})); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_timeouts.cpp b/example/corosio_timeouts.cpp new file mode 100644 index 000000000..a44d62117 --- /dev/null +++ b/example/corosio_timeouts.cpp @@ -0,0 +1,89 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; +using namespace std::chrono_literals; + +capy::io_task<> run_request(co_connection& conn) +{ + // A request containing only a ping command. + request req; + req.push("PING", "Hello world"); + + // Response where the PONG response will be stored. + response resp; + + // Executes the request with a timeout. If the server is down, + // exec will wait until it's back again, so it may suspend for a long time. + // For this reason, it's good practice to set a timeout to requests with capy::timeout. + // If the request hasn't completed after 10 seconds, exec is cancelled + // and the awaitable returns an error. + auto [ec] = co_await capy::timeout(conn.exec(req, resp), 10s); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } + + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; + co_return {}; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(config{})); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/example/corosio_unix_sockets.cpp b/example/corosio_unix_sockets.cpp new file mode 100644 index 000000000..958cdc795 --- /dev/null +++ b/example/corosio_unix_sockets.cpp @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +capy::io_task<> run_request(co_connection& conn) +{ + request req; + req.push("PING"); + + response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } + + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; + co_return {}; +} + +capy::task co_main() +{ + // If unix_socket is set to a non-empty string, UNIX domain sockets will be used + // instead of TCP. Set this value to the path where your server is listening. + // UNIX domain socket connections work in the same way as TCP connections. + config cfg; + cfg.unix_socket = "/tmp/redis-socks/redis.sock"; + + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp new file mode 100644 index 000000000..b4046f4e0 --- /dev/null +++ b/include/boost/redis/co_connection.hpp @@ -0,0 +1,415 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_CO_CONNECTION_HPP +#define BOOST_REDIS_CO_CONNECTION_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace boost::redis { +namespace detail { + +struct co_connection_impl; + +} // namespace detail + +/** @brief A connection to a Redis server, designed for capy coroutines. + * + * This class keeps a healthy connection to the Redis instance where + * commands can be sent at any time. For more details, please see the + * documentation of each individual function. + * + * Each I/O member function returns a `capy::io_task` that should + * be awaited from a coroutine running on a capy executor. + * This class uses corosio for sockets, TLS and timers, and does not depend on Boost.Asio. + * Cancellation follows the usual capy patterns, and is driven by the parent task's + * `std::stop_token`. When stop is requested, operations complete with + * an error matching `capy::cond::canceled`. + * + * This type is movable but not copyable. + * + * @par Thread safety + * Distinct objects: safe. + * Shared objects: unsafe. + * This class is not thread-safe: for a single object, if you + * call its member functions concurrently from separate threads, you will get a race condition. + * Use a strand if if you require thread safety. + */ +class co_connection { +public: + /** @brief Constructor from an execution context. + * + * @param ctx Execution context used to create all internal I/O objects. + * @param tls_ctx TLS context, used to create any required TLS streams. + * Used only when @ref config::use_ssl is `true`. + * The connection is usable with TLS even if not specified - + * a default-constructed TLS context is used in this case. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit co_connection( + capy::execution_context& ctx, + corosio::tls_context tls_ctx = {}, + logger lgr = {}); + + /** @brief Constructor from a capy executor. + * + * Equivalent to constructing from `ex.context()`. + * + * @param ex Executor whose context will own this connection. + * @param tls_ctx TLS context, used to create any required TLS streams. + * Used only when @ref config::use_ssl is `true`. + * The connection is usable with TLS even if not specified - + * a default-constructed TLS context is used in this case. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + template + co_connection(const Ex& ex, corosio::tls_context tls_ctx = {}, logger lgr = {}) + : co_connection(ex.context(), std::move(tls_ctx), std::move(lgr)) + { } + + /** @brief Constructor from an execution context and a logger. + * + * A TLS context with default settings will be created. + * + * @param ctx Execution context used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + co_connection(capy::execution_context& ctx, logger lgr); + + /** @brief Constructor from a capy executor and a logger. + * + * Equivalent to constructing from `ex.context()`. + * A TLS context with default settings will be created. + * + * @param ex Executor whose context will own this connection. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + template + co_connection(const Ex& ex, logger lgr) + : co_connection(ex.context(), corosio::tls_context{}, std::move(lgr)) + { } + + /** @brief Move constructor. + * + * Transfers ownership of the connection's internal state from `other` to + * the new object. Any operations that were in flight on `other` continue to make + * progress on the same internal state, now owned by `*this`. + * + * After the move, `other` is in a valid but unspecified state, + * and can only be assigned to or destroyed. + * + * @par Exception safety + * No-throw guarantee. + */ + co_connection(co_connection&&) noexcept; + + /** @brief Move assignment operator. + * + * Releases the resources currently owned by `*this` (if any) and + * transfers ownership of `other`'s internal state to `*this`. + * + * Any operations that were in flight on `other` continue to make + * progress on the same internal state, now owned by `*this`. + * If `*this` has any operation in flight, the behavior is undefined. + * + * After the move, `other` is in a valid but unspecified state, + * and can only be assigned to or destroyed. + * + * @par Exception safety + * No-throw guarantee. + * + * @return Reference to `*this`. + */ + co_connection& operator=(co_connection&&) noexcept; + + /// Destructor. + ~co_connection(); + + /** @brief Starts the underlying connection operations. + * + * This function establishes a connection to the Redis server and keeps + * it healthy by performing the following operations: + * + * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), + * contacts Sentinels to obtain the address of the configured master. + * @li For TCP connections, resolves the server hostname passed in + * @ref config::addr. + * @li Establishes a physical connection to the server. For TCP connections, + * connects to one of the endpoints obtained during name resolution. + * For UNIX domain socket connections, it connects to @ref config::unix_socket. + * @li If @ref config::use_ssl is `true`, performs the TLS handshake. + * @li Executes the setup request, as defined by the passed @ref config object. + * By default, this is a `HELLO` command, but it can contain any other arbitrary + * commands. See the @ref config::setup docs for more info. + * @li Starts a health-check operation where `PING` commands are sent + * at intervals specified by + * @ref config::health_check_interval when the connection is idle. + * See the documentation of @ref config::health_check_interval for more info. + * @li Starts read and write operations. Requests submitted via @ref exec + * before this task is ready to send data will be queued and written to + * the server as soon as the connection is up. + * + * When a connection is lost for any reason, a new one is + * established automatically. To disable reconnection + * set @ref config::reconnect_wait_interval to zero. + * + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: + * + * @code + * auto [ec] = co_await conn.run(cfg); + * @endcode + * + * If the passed configuration contains a critical error + * (e.g. TLS is enabled together with UNIX sockets), + * the operation completes immediately with a non-empty error code. + * + * If reconnection is diabled, the operation completes + * once an event that would otherwise trigger a reconnection is encountered. + * An informative error code is returned. + * + * If reconnection is enabled, the operation only completes when cancelled. + * + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. + * + * For an example on how to call this function refer to + * corosio_intro.cpp or any other corosio example. + * + * @param cfg Configuration parameters. + * + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. + */ + capy::io_task<> run(config const& cfg); + + /** @brief Wait for server pushes. + * + * This function suspends until at least one server push is received by the + * connection. On completion, an unspecified number of pushes will have been + * added to the response object set with @ref set_receive_response. Use the + * functions in the response object to know how many messages were received + * and consume them. + * + * To prevent receiving an unbounded number of pushes, the connection blocks + * further read operations on the socket when its internal receive buffer + * fills up. When that happens, in-flight @ref exec calls and health checks + * won't make any progress and the connection may eventually time out. To + * avoid this, applications that expect server pushes should call this + * function continuously in a loop. + * + * This function does *not* remove messages from the response object + * passed to @ref set_receive_response. Use the functions in the response + * object to achieve this. + * + * Only a single instance of `receive` may be outstanding for a given + * connection at any time. Launching a second `receive` fails + * with @ref error::already_running. + * + * `receive` does not complete when reconnection happens or + * when @ref run completes. + * + * @note To avoid deadlocks, the task calling `receive` should not also call + * `exec` in a way where they can block each other. That is, avoid the + * following pattern: + * + * @code + * capy::task receiver(co_connection& conn) + * { + * // Do NOT do this!!! The receive buffer might get full while + * // exec runs, which will block all read operations until receive + * // is called. The two operations end up waiting for each other, + * // making the connection unresponsive. If you need this pattern, + * // use two connections instead. + * co_await conn.receive(); + * co_await conn.exec(req, resp); + * } + * @endcode + * + * For an example see corosio_subscriber.cpp. + * + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: + * + * @code + * auto [ec] = co_await conn.receive(); + * @endcode + * + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. + * + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. + */ + capy::io_task<> receive(); + + /** @brief Executes commands on the Redis server. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. 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. + * + * For an example see corosio_intro.cpp. + * + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: + * + * @code + * auto [ec] = co_await conn.exec(req, resp); + * @endcode + * + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. + * + * What happens to the request depends on its state when cancellation is requested: + * + * @li If the request hasn't been sent to the server yet, cancellation will + * prevent it from being sent. + * @li If the request has been sent but the response hasn't arrived yet, + * cancellation causes `exec` to complete immediately. When the response + * eventually arrives from the server, it will be ignored. + * + * @par Object lifetimes + * Both `req` and `resp` should be kept alive until the operation completes. + * No copies of the request object are made. After `exec` completes, the objects + * can be safely destroyed, even if `exec` was cancelled. + * + * @param req The request to be executed. + * @param resp The response object to parse data into. + * + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. + */ + template + capy::io_task<> exec(request const& req, Response& resp = ignore) + { + return exec(req, any_adapter{resp}); + } + + /** @brief Executes commands on the Redis server. + * + * This is the type-erased version of `exec`. + * It has the same semantics as the typed `exec` overload + * Same as the other @ref exec overload, but takes a type-erased + * @ref any_adapter instead of a typed response. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. 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. + * + * For an example see corosio_intro.cpp. + * + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: + * + * @code + * auto [ec] = co_await conn.exec(req, resp); + * @endcode + * + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. + * + * What happens to the request depends on its state when cancellation is requested: + * + * @li If the request hasn't been sent to the server yet, cancellation will + * prevent it from being sent. + * @li If the request has been sent but the response hasn't arrived yet, + * cancellation causes `exec` to complete immediately. When the response + * eventually arrives from the server, it will be ignored. + * + * @par Object lifetimes + * Both `req` and any response object referenced by `adapter` + * should be kept alive until the operation completes. + * No copies of the request object are made. + * + * @param req The request to be executed. + * @param adapter An adapter object referencing a response to place data into. + * + * @return A task that yields a @ref boost::capy::io_result holding the + * operation's `std::error_code` on completion. + */ + capy::io_task<> exec(request const& req, any_adapter adapter); + + /** + * @brief Sets the response object for @ref receive operations. + * + * Pushes received by the connection (concretely, by @ref run) + * will be stored in `resp`. This happens even if @ref receive + * is not being called. + * + * `resp` should be able to accommodate the following message types: + * + * @li Any kind of RESP3 pushes that the application might expect. + * This usually involves Pub/Sub messages of type `message`, + * `subscribe`, `unsubscribe`, `psubscribe` and `punsubscribe`. + * See this page + * for more info. + * @li Any errors caused by failed `SUBSCRIBE` commands. Because of protocol + * oddities, these are placed in the receive buffer rather than handed to + * @ref exec. + * @li If your application is using `MONITOR`, simple strings. + * + * Because receive responses need to accommodate many different kinds + * of messages, it's advised to use one of the generic responses + * (like @ref generic_flat_response). If a response can't accommodate + * one of the received types, @ref run will exit with an error. + * + * Messages received before this function is called are discarded. + * + * @par Object lifetimes + * `resp` should be kept alive until @ref run completes. + */ + template + void set_receive_response(Response& resp) + { + set_receive_adapter(any_adapter(resp)); + } + + /// Returns connection usage information. + usage get_usage() const noexcept; + +private: + void set_receive_adapter(any_adapter adapter); + + std::unique_ptr impl_; +}; + +} // namespace boost::redis + +#endif // BOOST_REDIS_CO_CONNECTION_HPP diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 46184c2c1..c5137252f 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,22 @@ inline std::chrono::steady_clock::time_point compute_expiry( : std::chrono::steady_clock::now() + timeout; } +// Translates Asio's cancellation type into the common one used by the FSMs. +// Enumerator values match but are not guaranteed to remain like this. +// This function also filters any unknown Asio cancellation types. +// TODO: unit test +constexpr cancellation_type to_redis_cancellation(asio::cancellation_type_t t) noexcept +{ + int res = 0; + if ((t & asio::cancellation_type::terminal) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::terminal); + if ((t & asio::cancellation_type::partial) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::partial); + if ((t & asio::cancellation_type::total) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::total); + return static_cast(res); +} + template struct connection_impl { using clock_type = std::chrono::steady_clock; @@ -112,7 +129,7 @@ struct connection_impl { auto act = fsm_.resume( obj_->is_open(), obj_->st_, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); // Do what the FSM said switch (act.type()) { @@ -263,7 +280,10 @@ struct receive2_op { template void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) { - receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + receive_action act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case receive_action::action_type::setup_cancellation: @@ -304,7 +324,7 @@ struct exec_one_op { conn_->st_.mpx.get_read_buffer(), ec, bytes_written, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case exec_one_action_type::done: self.complete(act.ec); return; @@ -346,7 +366,10 @@ struct sentinel_resolve_op { void operator()(Self& self, system::error_code ec = {}) { auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + sentinel_action act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.get_type()) { case sentinel_action::type::done: self.complete(act.error()); return; @@ -382,7 +405,8 @@ auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& t template struct writer_op { connection_impl* conn_; - writer_fsm fsm_; + // asio timeouts => operation_aborted + writer_fsm fsm_{system::error_code(asio::error::operation_aborted).default_error_condition()}; explicit writer_op(connection_impl& conn) noexcept : conn_(&conn) @@ -396,7 +420,7 @@ struct writer_op { conn->st_, ec, bytes_written, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type()) { case writer_action_type::done: self.complete(act.error()); return; @@ -419,7 +443,8 @@ struct writer_op { template struct reader_op { connection_impl* conn_; - reader_fsm fsm_; + // asio timeouts => operation_aborted + reader_fsm fsm_{system::error_code(asio::error::operation_aborted).default_error_condition()}; public: reader_op(connection_impl& conn) noexcept @@ -431,7 +456,11 @@ struct reader_op { { for (;;) { auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); + auto act = fsm_.resume( + conn->st_, + n, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.get_type()) { case reader_fsm::action::type::read_some: @@ -459,7 +488,7 @@ template class run_op { private: connection_impl* conn_; - run_fsm fsm_{}; + run_fsm fsm_{unix_sockets_supported()}; template auto reader(CompletionToken&& token) @@ -498,7 +527,10 @@ class run_op { template void operator()(Self& self, system::error_code ec = {}) { - auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + auto act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case run_action_type::done: self.complete(act.ec); return; diff --git a/include/boost/redis/detail/cancellation_type.hpp b/include/boost/redis/detail/cancellation_type.hpp new file mode 100644 index 000000000..fbdba6c3f --- /dev/null +++ b/include/boost/redis/detail/cancellation_type.hpp @@ -0,0 +1,52 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_CANCELLATION_TYPE_HPP +#define BOOST_REDIS_CANCELLATION_TYPE_HPP + +// A minimal port of asio::cancellation_type_t to avoid +// depending on Asio in the protocol state machines + +namespace boost::redis::detail { + +enum class cancellation_type : int +{ + none = 0, + terminal = 1, + partial = 2, + total = 4, +}; + +constexpr cancellation_type operator|(cancellation_type lhs, cancellation_type rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +constexpr bool contains(cancellation_type value, cancellation_type query) +{ + return (static_cast(value) & static_cast(query)) != 0; +} + +constexpr bool contains_terminal(cancellation_type value) +{ + return contains(value, cancellation_type::terminal); +} + +constexpr bool contains_partial(cancellation_type value) +{ + return contains(value, cancellation_type::partial); +} + +constexpr bool contains_total(cancellation_type value) +{ + return contains(value, cancellation_type::total); +} + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/detail/co_connect_fsm.hpp b/include/boost/redis/detail/co_connect_fsm.hpp new file mode 100644 index 000000000..ee74dadaa --- /dev/null +++ b/include/boost/redis/detail/co_connect_fsm.hpp @@ -0,0 +1,73 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_CO_CONNECT_FSM_HPP +#define BOOST_REDIS_CO_CONNECT_FSM_HPP + +#include + +#include +#include +#include + +#include + +// Sans-io algorithm for redis_stream::async_connect, as a finite state machine + +namespace boost::redis::detail { + +struct buffered_logger; + +// What should we do next? +enum class co_connect_action_type +{ + unix_socket_connect, // Connect to the UNIX socket + tcp_resolve, // Name resolution + tcp_connect, // TCP connect + ssl_handshake, // SSL handshake + done, // Complete the async op +}; + +struct co_connect_action { + co_connect_action_type type; + system::error_code ec; + + co_connect_action(co_connect_action_type type) noexcept + : type{type} + { } + + co_connect_action(system::error_code ec) noexcept + : type{co_connect_action_type::done} + , ec{ec} + { } +}; + +class co_connect_fsm { + int resume_point_{0}; + buffered_logger* lgr_{nullptr}; + transport_type type_; + +public: + co_connect_fsm(buffered_logger& lgr, transport_type type) noexcept + : lgr_(&lgr) + , type_(type) + { } + + co_connect_action resume( + system::error_code ec, + std::span resolver_results); + + co_connect_action resume(system::error_code ec, const corosio::endpoint& selected_endpoint); + + co_connect_action resume(system::error_code ec); + +}; // namespace boost::redis::detail + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/detail/connect_fsm.hpp b/include/boost/redis/detail/connect_fsm.hpp index 352355667..d04b76470 100644 --- a/include/boost/redis/detail/connect_fsm.hpp +++ b/include/boost/redis/detail/connect_fsm.hpp @@ -9,6 +9,8 @@ #ifndef BOOST_REDIS_CONNECT_FSM_HPP #define BOOST_REDIS_CONNECT_FSM_HPP +#include + #include #include #include @@ -19,14 +21,6 @@ namespace boost::redis::detail { struct buffered_logger; -// What transport is redis_stream using? -enum class transport_type -{ - tcp, // plaintext TCP - tcp_tls, // TLS over TCP - unix_socket, // UNIX domain sockets -}; - struct redis_stream_state { transport_type type{transport_type::tcp}; bool ssl_stream_used{false}; diff --git a/include/boost/redis/detail/connect_params.hpp b/include/boost/redis/detail/connect_params.hpp index e0dfa01d0..d1d32b005 100644 --- a/include/boost/redis/detail/connect_params.hpp +++ b/include/boost/redis/detail/connect_params.hpp @@ -12,7 +12,7 @@ // Parameters used by redis_stream::async_connect #include -#include +#include #include #include diff --git a/include/boost/redis/detail/exec_fsm.hpp b/include/boost/redis/detail/exec_fsm.hpp index 7dc2f8c12..a96038304 100644 --- a/include/boost/redis/detail/exec_fsm.hpp +++ b/include/boost/redis/detail/exec_fsm.hpp @@ -9,9 +9,9 @@ #ifndef BOOST_REDIS_EXEC_FSM_HPP #define BOOST_REDIS_EXEC_FSM_HPP +#include #include -#include #include #include @@ -66,7 +66,7 @@ class exec_fsm { exec_action resume( bool connection_is_open, connection_state& st, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/exec_one_fsm.hpp b/include/boost/redis/detail/exec_one_fsm.hpp index 4ad3bd3ec..cb6061c2e 100644 --- a/include/boost/redis/detail/exec_one_fsm.hpp +++ b/include/boost/redis/detail/exec_one_fsm.hpp @@ -10,9 +10,9 @@ #define BOOST_REDIS_EXEC_ONE_FSM_HPP #include +#include #include -#include #include #include @@ -61,7 +61,7 @@ class exec_one_fsm { read_buffer& buffer, system::error_code ec, std::size_t bytes_transferred, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/flow_controller.hpp b/include/boost/redis/detail/flow_controller.hpp new file mode 100644 index 000000000..aa156652c --- /dev/null +++ b/include/boost/redis/detail/flow_controller.hpp @@ -0,0 +1,78 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_DETAIL_FLOW_CONTROLLER_HPP +#define BOOST_REDIS_DETAIL_FLOW_CONTROLLER_HPP + +#include +#include +#include +#include + +#include +#include + +namespace boost::redis::detail { + +class flow_controller { + std::size_t pending_bytes_{}; + std::size_t max_bytes_; + capy::async_event bytes_available_; + capy::async_event room_available_; + +public: + flow_controller(std::size_t max_bytes) noexcept + : max_bytes_(max_bytes) + { + room_available_.set(); + assert(max_bytes != 0u); + } + + /** Waits until at least one byte has been put in the flow controller. */ + capy::io_task<> take() + { + while (pending_bytes_ == 0u) { + auto [ec] = co_await bytes_available_.wait(); + if (ec) + co_return {ec}; + } + pending_bytes_ = 0u; + bytes_available_.clear(); + room_available_.set(); + co_return {}; + } + + bool try_put(std::size_t bytes) + { + // Do we have space? + if (!room_available_.is_set()) + return false; + + // Add the bytes. We might surpass the limit slightly, but this is OK + // because we've already read the bytes. + // The following messages will wait + pending_bytes_ += bytes; + if (pending_bytes_ >= max_bytes_) + room_available_.clear(); + bytes_available_.set(); + + return true; + } + + capy::io_task<> put(std::size_t bytes) + { + while (!try_put(bytes)) { + auto [ec] = co_await room_available_.wait(); + if (ec) + co_return {ec}; + } + co_return {}; + } +}; + +} // namespace boost::redis::detail + +#endif // BOOST_REDIS_FLOW_CONTROLLER_HPP diff --git a/include/boost/redis/detail/reader_fsm.hpp b/include/boost/redis/detail/reader_fsm.hpp index 3e7a70321..29ddf8a6b 100644 --- a/include/boost/redis/detail/reader_fsm.hpp +++ b/include/boost/redis/detail/reader_fsm.hpp @@ -7,14 +7,15 @@ #ifndef BOOST_REDIS_READER_FSM_HPP #define BOOST_REDIS_READER_FSM_HPP +#include #include #include -#include #include #include #include +#include namespace boost::redis::detail { @@ -83,11 +84,14 @@ class reader_fsm { connection_state& st, std::size_t bytes_read, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); - reader_fsm() = default; + reader_fsm(std::error_condition timeout_cond) noexcept + : timeout_cond_(timeout_cond) + { } private: + std::error_condition timeout_cond_; int resume_point_{0}; std::pair res_{consume_result::needs_more, 0u}; }; diff --git a/include/boost/redis/detail/receive_fsm.hpp b/include/boost/redis/detail/receive_fsm.hpp index 16989dce0..d9812dfd1 100644 --- a/include/boost/redis/detail/receive_fsm.hpp +++ b/include/boost/redis/detail/receive_fsm.hpp @@ -9,7 +9,8 @@ #ifndef BOOST_REDIS_RECEIVE_FSM_HPP #define BOOST_REDIS_RECEIVE_FSM_HPP -#include +#include + #include // Sans-io algorithm for async_receive2, as a finite state machine @@ -50,7 +51,7 @@ class receive_fsm { receive_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/redis_stream.hpp b/include/boost/redis/detail/redis_stream.hpp index 349cc650d..ddbbe2b9e 100644 --- a/include/boost/redis/detail/redis_stream.hpp +++ b/include/boost/redis/detail/redis_stream.hpp @@ -34,6 +34,15 @@ namespace boost { namespace redis { namespace detail { +constexpr bool unix_sockets_supported() +{ +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + return true; +#else + return false; +#endif +} + template class redis_stream { asio::ssl::context ssl_ctx_; diff --git a/include/boost/redis/detail/run_fsm.hpp b/include/boost/redis/detail/run_fsm.hpp index b125fa746..95a342486 100644 --- a/include/boost/redis/detail/run_fsm.hpp +++ b/include/boost/redis/detail/run_fsm.hpp @@ -9,9 +9,9 @@ #ifndef BOOST_REDIS_RUN_FSM_HPP #define BOOST_REDIS_RUN_FSM_HPP +#include #include -#include #include // Sans-io algorithm for async_run, as a finite state machine @@ -48,16 +48,19 @@ struct run_action { }; class run_fsm { + bool unix_sockets_supported_; int resume_point_{0}; system::error_code stored_ec_; public: - run_fsm() = default; + run_fsm(bool unix_sockets_supported) noexcept + : unix_sockets_supported_(unix_sockets_supported) + { } run_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; connect_params make_run_connect_params(const connection_state& st); diff --git a/include/boost/redis/detail/sentinel_resolve_fsm.hpp b/include/boost/redis/detail/sentinel_resolve_fsm.hpp index de8d4db69..05b092ccb 100644 --- a/include/boost/redis/detail/sentinel_resolve_fsm.hpp +++ b/include/boost/redis/detail/sentinel_resolve_fsm.hpp @@ -11,9 +11,9 @@ #include #include +#include #include -#include #include #include @@ -82,7 +82,7 @@ class sentinel_resolve_fsm { sentinel_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr); diff --git a/include/boost/redis/detail/transport_type.hpp b/include/boost/redis/detail/transport_type.hpp new file mode 100644 index 000000000..dc386561f --- /dev/null +++ b/include/boost/redis/detail/transport_type.hpp @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_TRANSPORT_TYPE_HPP +#define BOOST_REDIS_TRANSPORT_TYPE_HPP + +namespace boost::redis::detail { + +// What transport are we using? +enum class transport_type +{ + tcp, // plaintext TCP + tcp_tls, // TLS over TCP + unix_socket, // UNIX domain sockets +}; + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/detail/write.hpp b/include/boost/redis/detail/write.hpp deleted file mode 100644 index 69b136f42..000000000 --- a/include/boost/redis/detail/write.hpp +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_WRITE_HPP -#define BOOST_REDIS_WRITE_HPP - -#include - -#include - -namespace boost::redis::detail { - -/** @brief Writes a request synchronously. - * - * @param stream Stream to write the request to. - * @param req Request to write. - */ -template -auto write(SyncWriteStream& stream, request const& req) -{ - return asio::write(stream, asio::buffer(req.payload())); -} - -template -auto write(SyncWriteStream& stream, request const& req, system::error_code& ec) -{ - return asio::write(stream, asio::buffer(req.payload()), ec); -} - -/** @brief Writes a request asynchronously. - * - * @param stream Stream to write the request to. - * @param req Request to write. - * @param token Asio completion token. - */ -template < - class AsyncWriteStream, - class CompletionToken = - asio::default_completion_token_t > -auto async_write( - AsyncWriteStream& stream, - request const& req, - CompletionToken&& token = - asio::default_completion_token_t{}) -{ - return asio::async_write(stream, asio::buffer(req.payload()), token); -} - -} // namespace boost::redis::detail - -#endif // BOOST_REDIS_WRITE_HPP diff --git a/include/boost/redis/detail/writer_fsm.hpp b/include/boost/redis/detail/writer_fsm.hpp index be2858349..807501a12 100644 --- a/include/boost/redis/detail/writer_fsm.hpp +++ b/include/boost/redis/detail/writer_fsm.hpp @@ -9,12 +9,14 @@ #ifndef BOOST_REDIS_WRITER_FSM_HPP #define BOOST_REDIS_WRITER_FSM_HPP -#include +#include + #include #include #include #include +#include // Sans-io algorithm for the writer task, as a finite state machine @@ -75,16 +77,19 @@ class writer_action { }; class writer_fsm { + std::error_condition timeout_cond_; int resume_point_{0}; public: - writer_fsm() = default; + writer_fsm(std::error_condition timeout_cond) noexcept + : timeout_cond_(timeout_cond) + { } writer_action resume( connection_state& st, system::error_code ec, std::size_t bytes_written, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/impl/co_connect_fsm.ipp b/include/boost/redis/impl/co_connect_fsm.ipp new file mode 100644 index 000000000..b4582184b --- /dev/null +++ b/include/boost/redis/impl/co_connect_fsm.ipp @@ -0,0 +1,160 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace boost::redis::detail { + +// Logging +inline void format_tcp_endpoint(const corosio::endpoint& ep, std::string& to) +{ + if (ep.is_v6()) { + to += '['; + to += ep.v6_address().to_string(); + to += ']'; + } else { + to += ep.v4_address().to_string(); + } + to += ':'; + to += std::to_string(ep.port()); +} + +template <> +struct log_traits { + static inline void log(std::string& to, const corosio::endpoint& value) + { + format_tcp_endpoint(value, to); + } +}; + +template <> +struct log_traits> { + static inline void log(std::string& to, std::span value) + { + auto iter = value.begin(); + auto end = value.end(); + + if (iter != end) { + format_tcp_endpoint(iter->get_endpoint(), to); + ++iter; + for (; iter != end; ++iter) { + to += ", "; + format_tcp_endpoint(iter->get_endpoint(), to); + } + } + } +}; + +co_connect_action co_connect_fsm::resume( + system::error_code ec, + std::span resolver_results) +{ + // Log it + if (ec) { + log_info(*lgr_, "Connect: hostname resolution failed: ", ec); + } else { + log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results); + } + + // Delegate to the regular resume function + return resume(ec); +} + +co_connect_action co_connect_fsm::resume( + system::error_code ec, + const corosio::endpoint& selected_endpoint) +{ + // Log it + if (ec) { + log_info(*lgr_, "Connect: TCP connect failed: ", ec); + } else { + log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint); + } + + // Delegate to the regular resume function + return resume(ec); +} + +co_connect_action co_connect_fsm::resume(system::error_code ec) +{ + switch (resume_point_) { + BOOST_REDIS_CORO_INITIAL + + if (type_ == transport_type::unix_socket) { + // Connect to the socket + BOOST_REDIS_YIELD(resume_point_, 1, co_connect_action_type::unix_socket_connect) + + // Log it + if (ec) { + log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec); + } else { + log_debug(*lgr_, "Connect: UNIX socket connect succeeded"); + } + + // If this failed, we can't continue + if (ec) { + return ec; + } + + // Done + return system::error_code(); + } else { + // Resolve names. The continuation needs access to the returned + // endpoints, and is a specialized resume() that will call this function + BOOST_REDIS_YIELD(resume_point_, 2, co_connect_action_type::tcp_resolve) + + // If this failed, we can't continue + if (ec) { + return ec; + } + + // Now connect to the endpoints returned by the resolver. + // This has a specialized resume(), too + BOOST_REDIS_YIELD(resume_point_, 3, co_connect_action_type::tcp_connect) + + // If this failed, we can't continue + if (ec) { + return ec; + } + + if (type_ == transport_type::tcp_tls) { + // Perform the TLS handshake + BOOST_REDIS_YIELD(resume_point_, 6, co_connect_action_type::ssl_handshake) + + // Log it + if (ec) { + log_info(*lgr_, "Connect: SSL handshake failed: ", ec); + } else { + log_debug(*lgr_, "Connect: SSL handshake succeeded"); + } + + // If this failed, we can't continue + if (ec) { + return ec; + } + } + + // Done + return system::error_code(); + } + } + + BOOST_ASSERT(false); + return system::error_code(); +} + +} // namespace boost::redis::detail diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp new file mode 100644 index 000000000..30cf64936 --- /dev/null +++ b/include/boost/redis/impl/co_connection.ipp @@ -0,0 +1,498 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis { +namespace detail { + +// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" +inline std::chrono::steady_clock::time_point compute_expiry( + std::chrono::steady_clock::duration timeout) +{ + return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; +} + +inline cancellation_type to_cancel(std::stop_token tok) +{ + return tok.stop_requested() ? cancellation_type::terminal : cancellation_type::none; +} + +// Run an operation with a timeout, with a zero timeout meaning 'no timeout' +template +capy::task> maybe_timeout( + Aw aw, + std::chrono::steady_clock::duration timeout) +{ + if (timeout.count() == 0) + co_return co_await std::move(aw); + else + co_return co_await capy::timeout(std::move(aw), timeout); +} + +class co_redis_stream { + struct tcp_state { + corosio::resolver resolv; + corosio::tcp_socket sock; + + explicit tcp_state(capy::execution_context& ctx) + : resolv(ctx) + , sock(ctx) + { } + }; + + // Required to create the other objects + capy::execution_context& ctx_; + corosio::tls_context tls_ctx_; + + // Constructed lazily as required + std::optional tcp_; + std::optional unix_; + std::optional tls_; + + // Contains the stream that will end up being used + capy::any_stream stream_; + + void setup(transport_type type) + { + if (type == transport_type::unix_socket) { + if (unix_.has_value()) { + // UNIX sockets don't use range connect. + // We need to close and re-open the socket before establishing another connection + unix_->close(); + unix_->open(); + } else { + unix_.emplace(ctx_); + } + stream_ = capy::any_stream(&*unix_); + } else { + // TCP (with or without TLS) + // Allocate the object if not there. + // TCP uses range connect, so we don't need to close and reopen the socket + if (!tcp_.has_value()) + tcp_.emplace(ctx_); + + // If using TLS, allocate or reset the stream + if (type == transport_type::tcp_tls) { + if (tls_.has_value()) + tls_->reset(); + else + tls_.emplace(capy::any_stream(&tcp_->sock), tls_ctx_); + stream_ = capy::any_stream(&*tls_); + } else { + stream_ = capy::any_stream(&tcp_->sock); + } + } + } + +public: + explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) + : ctx_(ctx) + , tls_ctx_(std::move(tls_ctx)) + { } + + // I/O + capy::io_task<> connect(const connect_params& params, buffered_logger& l) + { + co_connect_fsm fsm{l, params.addr.type()}; + system::error_code ec; + corosio::resolver_results endpoints; + + setup(params.addr.type()); + + auto act = fsm.resume(ec); + + while (true) { + switch (act.type) { + case co_connect_action_type::unix_socket_connect: + { + auto result = co_await capy::timeout( + unix_->connect(corosio::local_endpoint(params.addr.unix_socket())), + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec); + break; + } + case co_connect_action_type::tcp_resolve: + { + auto result = co_await capy::timeout( + tcp_->resolv.resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port), + params.resolve_timeout); + ec = result.ec; + endpoints = std::move(std::get<0>(result.values)); + act = fsm.resume(ec, endpoints); + break; + } + case co_connect_action_type::ssl_handshake: + { + ec = (co_await capy::timeout( + tls_->handshake(corosio::tls_stream::handshake_type::client), + params.ssl_handshake_timeout)) + .ec; + act = fsm.resume(ec); + break; + } + case co_connect_action_type::done: co_return {act.ec}; + case co_connect_action_type::tcp_connect: + { + auto result = co_await capy::timeout( + corosio::connect(tcp_->sock, std::move(endpoints)), + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec, result.get<1>()); + break; + } + default: BOOST_ASSERT(false); + } + } + } + + template + auto write_some(BuffType&& buffers) + { + return stream_.write_some(std::forward(buffers)); + } + + template + auto read_some(BuffType&& buffers) + { + return stream_.read_some(std::forward(buffers)); + } +}; + +struct co_connection_impl { + capy::async_event run_cancelled_event_; + co_redis_stream stream_; + corosio::timer writer_timer_; // timer used for write timeouts + corosio::timer writer_cv_; // set when there is new data to write + corosio::timer reader_timer_; // timer used for read timeouts + corosio::timer reconnect_timer_; // to wait the reconnection period + corosio::timer ping_timer_; // to wait between pings + flow_controller controller_; + connection_state st_; + + co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr) + : stream_{ctx, std::move(ssl_ctx)} + , writer_timer_{ctx} + , writer_cv_{ctx} + , reader_timer_{ctx} + , reconnect_timer_{ctx} + , ping_timer_{ctx} + , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable + , st_{{std::move(lgr)}} + { + set_receive_adapter(any_adapter{ignore}); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); + } + + void set_receive_adapter(any_adapter adapter) + { + st_.mpx.set_receive_adapter(std::move(adapter)); + } + + capy::io_task<> exec(request const& req, any_adapter adapter) + { + // Setup + capy::async_event request_done; + auto elem = make_elem(req, std::move(adapter)); + elem->set_done_callback([&request_done]() { + request_done.set(); + }); + exec_fsm fsm{elem}; + + // Invoke the FSM + while (true) { + // Invoke the state machine + auto act = fsm.resume(true, st_, to_cancel(co_await capy::this_coro::stop_token)); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: break; // ignored, not required by capy + case exec_action_type::immediate: break; // ignored, not required by capy + case exec_action_type::notify_writer: writer_cv_.cancel(); break; + case exec_action_type::wait_for_response: + { + auto [ec] = co_await request_done.wait(); + ignore_unused(ec); // TODO: we should likely use this + break; + } + case exec_action_type::done: co_return {act.error()}; + } + } + } + + capy::io_task<> receive() + { + // Setup + receive_fsm fsm; + system::error_code ec; + + while (true) { + receive_action act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case receive_action::action_type::setup_cancellation: break; // not required here + case receive_action::action_type::wait: + { + auto [controller_ec] = co_await controller_.take(); + ec = controller_ec; + break; + } + case receive_action::action_type::drain_channel: break; // not required + case receive_action::action_type::immediate: break; // not required + case receive_action::action_type::done: co_return {act.ec}; + } + } + } + + capy::io_task<> exec_one(const request& req, any_adapter resp) + { + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + auto& rdbuff = st_.mpx.get_read_buffer(); + + // First invocation + auto act = fsm.resume(rdbuff, system::error_code(), 0u, cancellation_type::none); + + while (true) { + switch (act.type) { + case exec_one_action_type::done: co_return {act.ec}; + case exec_one_action_type::write: + { + auto [ec, bytes] = co_await capy::write(stream_, capy::make_buffer(req.payload())); + act = fsm.resume(rdbuff, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + case exec_one_action_type::read_some: + { + // https://github.com/cppalliance/capy/issues/147 + auto buff = rdbuff.get_prepared(); + auto [ec, bytes] = co_await stream_.read_some( + capy::mutable_buffer(buff.data(), buff.size())); + act = fsm.resume(rdbuff, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + } + } + } + + capy::io_task<> sentinel_resolve() + { + // Setup + sentinel_resolve_fsm fsm; + auto act = fsm.resume(st_, system::error_code(), cancellation_type::none); + + while (true) { + switch (act.get_type()) { + case sentinel_action::type::done: co_return {act.error()}; + case sentinel_action::type::connect: + { + auto [ec] = co_await stream_.connect( + make_sentinel_connect_params(st_.cfg, act.connect_addr()), + st_.logger); + act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + case sentinel_action::type::request: + { + auto [ec] = co_await capy::timeout( + exec_one(st_.cfg.sentinel.setup, make_sentinel_adapter(st_)), + st_.cfg.sentinel.request_timeout); + act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + } + } + } + + // This signature is required because capy::when_any is equivalent to wait_for_one_success + capy::io_task writer() + { + // Setup. + writer_fsm fsm{capy::cond::timeout}; + auto act = fsm.resume(st_, system::error_code(), 0u, cancellation_type::none); + + while (true) { + switch (act.type()) { + case writer_action_type::done: co_return {{}, act.error()}; + case writer_action_type::write_some: + { + auto [ec, bytes] = co_await maybe_timeout( + stream_.write_some(capy::make_buffer(st_.mpx.get_write_buffer())), + act.timeout()); + act = fsm.resume(st_, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + case writer_action_type::wait: + { + writer_cv_.expires_at(compute_expiry(act.timeout())); + auto [ec] = co_await writer_cv_.wait(); + act = fsm.resume(st_, ec, 0u, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + } + } + } + + capy::io_task reader() + { + reader_fsm fsm{capy::cond::timeout}; + auto act = fsm.resume(st_, 0u, system::error_code(), cancellation_type::none); + + for (;;) { + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + { + // https://github.com/cppalliance/capy/issues/147 + auto buff = st_.mpx.get_prepared_read_buffer(); + auto [ec, bytes] = co_await maybe_timeout( + stream_.read_some(capy::mutable_buffer(buff.data(), buff.size())), + act.timeout()); + act = fsm.resume(st_, bytes, ec, to_cancel(co_await capy::this_coro::stop_token)); + break; + } + case reader_fsm::action::type::notify_push_receiver: + { + auto cancel = to_cancel(co_await capy::this_coro::stop_token); + if (controller_.try_put(act.push_size())) { + act = fsm.resume(st_, 0u, system::error_code(), cancel); + } else { + auto [ec] = co_await controller_.put(act.push_size()); + act = fsm.resume(st_, 0u, ec, cancel); + } + break; + } + case reader_fsm::action::type::done: co_return {{}, act.error()}; + } + } + } + + capy::io_task<> run(const config& cfg) + { + // corosio only runs in systems that support UNIX sockets + constexpr bool unix_sockets_supported = true; + run_fsm fsm{unix_sockets_supported}; + system::error_code ec; + + // Setup + st_.cfg = cfg; + st_.mpx.set_config(cfg); + + while (true) { + auto act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case run_action_type::done: co_return {act.ec}; + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve()).ec; break; + case run_action_type::connect: + ec = (co_await stream_.connect(make_run_connect_params(st_), st_.logger)).ec; + break; + case run_action_type::parallel_group: + { + auto result = co_await capy::when_any(reader(), writer()); + ec = std::visit( + [](std::error_code value) { + return value; + }, + result); + break; + } + case run_action_type::cancel_receive: + case run_action_type::immediate: + ec = system::error_code(); + break; // no longer required + case run_action_type::wait_for_reconnection: + reconnect_timer_.expires_after(st_.cfg.reconnect_wait_interval); + ec = (co_await reconnect_timer_.wait()).ec; + break; + } + } + } +}; + +} // namespace detail + +co_connection::co_connection(capy::execution_context& ctx, corosio::tls_context ssl_ctx, logger lgr) +: impl_(std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) +{ } + +co_connection::co_connection(capy::execution_context& ctx, logger lgr) +: co_connection(ctx, {}, std::move(lgr)) +{ } + +co_connection::co_connection(co_connection&&) noexcept = default; +co_connection& co_connection::operator=(co_connection&&) noexcept = default; +co_connection::~co_connection() = default; + +capy::io_task<> co_connection::run(config const& cfg) { return impl_->run(cfg); } + +capy::io_task<> co_connection::receive() { return impl_->receive(); } + +capy::io_task<> co_connection::exec(request const& req, any_adapter adapter) +{ + return impl_->exec(req, std::move(adapter)); +} + +void co_connection::set_receive_adapter(any_adapter resp) +{ + impl_->set_receive_adapter(std::move(resp)); +} + +usage co_connection::get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } + +} // namespace boost::redis diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 3898d1e18..335299f52 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -9,30 +9,22 @@ #ifndef BOOST_REDIS_EXEC_FSM_IPP #define BOOST_REDIS_EXEC_FSM_IPP +#include #include #include #include +#include #include -#include #include +#include namespace boost::redis::detail { -inline bool is_partial_or_terminal_cancel(asio::cancellation_type_t type) -{ - return !!(type & (asio::cancellation_type_t::partial | asio::cancellation_type_t::terminal)); -} - -inline bool is_total_cancel(asio::cancellation_type_t type) -{ - return !!(type & asio::cancellation_type_t::total); -} - exec_action exec_fsm::resume( bool connection_is_open, connection_state& st, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -79,11 +71,11 @@ exec_action exec_fsm::resume( // Total cancellation can only be handled if the request hasn't been sent yet. // Partial and terminal cancellation can always be served if ( - (is_total_cancel(cancel_state) && elem_->is_waiting()) || - is_partial_or_terminal_cancel(cancel_state)) { + (contains_total(cancel_state) && elem_->is_waiting()) || + contains_partial(cancel_state) || contains_terminal(cancel_state)) { st.mpx.cancel(elem_); elem_.reset(); // Deallocate memory before finalizing - return exec_action{asio::error::operation_aborted}; + return exec_action{make_error_code(system::errc::operation_canceled)}; } } } diff --git a/include/boost/redis/impl/exec_one_fsm.ipp b/include/boost/redis/impl/exec_one_fsm.ipp index b4a7250b2..d222081b6 100644 --- a/include/boost/redis/impl/exec_one_fsm.ipp +++ b/include/boost/redis/impl/exec_one_fsm.ipp @@ -17,9 +17,8 @@ #include #include -#include -#include #include +#include #include #include @@ -30,7 +29,7 @@ exec_one_action exec_one_fsm::resume( read_buffer& buffer, system::error_code ec, std::size_t bytes_transferred, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -40,7 +39,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{asio::error::operation_aborted}; + return make_error_code(system::errc::operation_canceled); if (ec) return ec; @@ -61,7 +60,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{asio::error::operation_aborted}; + return make_error_code(system::errc::operation_canceled); if (ec) return ec; diff --git a/include/boost/redis/impl/is_terminal_cancel.hpp b/include/boost/redis/impl/is_terminal_cancel.hpp index 308e36473..b997c4cad 100644 --- a/include/boost/redis/impl/is_terminal_cancel.hpp +++ b/include/boost/redis/impl/is_terminal_cancel.hpp @@ -9,15 +9,15 @@ #ifndef BOOST_REDIS_IS_TERMINAL_CANCEL_HPP #define BOOST_REDIS_IS_TERMINAL_CANCEL_HPP -#include +#include namespace boost::redis::detail { -constexpr bool is_terminal_cancel(asio::cancellation_type_t cancel_state) +constexpr bool is_terminal_cancel(cancellation_type cancel_state) { - return (cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none; + return contains_terminal(cancel_state); } } // namespace boost::redis::detail -#endif \ No newline at end of file +#endif diff --git a/include/boost/redis/impl/multiplexer.ipp b/include/boost/redis/impl/multiplexer.ipp index b6d118ce2..255c35f93 100644 --- a/include/boost/redis/impl/multiplexer.ipp +++ b/include/boost/redis/impl/multiplexer.ipp @@ -5,10 +5,10 @@ */ #include +#include #include #include -#include #include #include @@ -243,7 +243,7 @@ std::size_t multiplexer::cancel_waiting() auto const ret = std::distance(point, std::end(reqs_)); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({asio::error::operation_aborted}); + ptr->notify_error({make_error_code(system::errc::operation_canceled)}); }); reqs_.erase(point, std::end(reqs_)); @@ -276,7 +276,7 @@ void multiplexer::cancel_on_conn_lost() auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({asio::error::operation_aborted}); + ptr->notify_error({make_error_code(system::errc::operation_canceled)}); }); reqs_.erase(point, std::end(reqs_)); diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index f04dff4d7..e73e00b79 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -11,16 +11,13 @@ #include #include -#include -#include - namespace boost::redis::detail { reader_fsm::action reader_fsm::resume( connection_state& st, std::size_t bytes_read, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -42,13 +39,13 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(make_error_code(system::errc::operation_canceled)); } - // Translate timeout errors caused by operation_aborted to more legible ones. + // Translate timeout errors to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. - if (ec == asio::error::operation_aborted) { + if (ec == timeout_cond_) { ec = error::pong_timeout; } @@ -95,7 +92,7 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(make_error_code(system::errc::operation_canceled)); } // Check for other errors. diff --git a/include/boost/redis/impl/receive_fsm.ipp b/include/boost/redis/impl/receive_fsm.ipp index ca98dcd8e..73a1f1784 100644 --- a/include/boost/redis/impl/receive_fsm.ipp +++ b/include/boost/redis/impl/receive_fsm.ipp @@ -6,24 +6,17 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include #include -#include #include #include namespace boost::redis::detail { -constexpr bool is_any_cancel(asio::cancellation_type_t type) -{ - return !!( - type & (asio::cancellation_type_t::terminal | asio::cancellation_type_t::partial | - asio::cancellation_type_t::total)); -} - // We use the receive2_cancelled flag rather than will_reconnect() to // avoid entanglement between async_run and async_receive2 cancellations. // If we had used will_reconnect(), async_receive2 would be cancelled @@ -31,7 +24,7 @@ constexpr bool is_any_cancel(asio::cancellation_type_t type) receive_action receive_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -61,9 +54,9 @@ receive_action receive_fsm::resume( } // Check for cancellations - if (is_any_cancel(cancel_state) || st.receive2_cancelled) { + if (cancel_state != cancellation_type::none || st.receive2_cancelled) { st.receive2_running = false; - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } // If we get any unknown errors, propagate them (shouldn't happen, but just in case) diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index 8315665e9..d8656ef59 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -19,23 +19,19 @@ #include #include -#include -#include -#include // for BOOST_ASIO_HAS_LOCAL_SOCKETS #include namespace boost::redis::detail { -inline system::error_code check_config(const config& cfg) +inline system::error_code check_config(const config& cfg, bool unix_sockets_supported) { if (!cfg.unix_socket.empty()) { if (cfg.use_ssl) return error::unix_sockets_ssl_unsupported; if (use_sentinel(cfg)) return error::sentinel_unix_sockets_unsupported; -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS - return error::unix_sockets_unsupported; -#endif + if (!unix_sockets_supported) + return error::unix_sockets_unsupported; } return system::error_code{}; } @@ -89,13 +85,13 @@ struct log_traits { run_action run_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL // Check config - ec = check_config(st.cfg); + ec = check_config(st.cfg, unix_sockets_supported_); if (ec) { log_err(st.logger, "Invalid configuration: ", ec); stored_ec_ = ec; @@ -126,7 +122,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (4)"); - return {asio::error::operation_aborted}; + return {make_error_code(system::errc::operation_canceled)}; } // Check for errors @@ -141,7 +137,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } if (ec) { @@ -193,7 +189,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } sleep_and_reconnect: @@ -209,7 +205,7 @@ sleep_and_reconnect: // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (3)"); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } } } diff --git a/include/boost/redis/impl/sentinel_resolve_fsm.ipp b/include/boost/redis/impl/sentinel_resolve_fsm.ipp index c4359e63b..e6ac36d74 100644 --- a/include/boost/redis/impl/sentinel_resolve_fsm.ipp +++ b/include/boost/redis/impl/sentinel_resolve_fsm.ipp @@ -20,7 +20,6 @@ #include #include -#include #include #include @@ -43,7 +42,7 @@ void log_sentinel_error(connection_state& st, std::size_t current_idx, const Arg sentinel_action sentinel_resolve_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -69,7 +68,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } // Check for errors @@ -86,7 +85,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } // Check for errors diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index 460f21c68..201967c46 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -19,9 +19,8 @@ #include #include -#include -#include #include +#include #include #include @@ -57,7 +56,7 @@ writer_action writer_fsm::resume( connection_state& st, system::error_code ec, std::size_t bytes_written, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -81,13 +80,13 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) - ec = asio::error::operation_aborted; - else if (ec == asio::error::operation_aborted) + ec = make_error_code(system::errc::operation_canceled); + else if (ec == timeout_cond_) ec = error::write_timeout; // Check for errors if (ec) { - if (ec == asio::error::operation_aborted) { + if (ec == system::errc::operation_canceled) { log_debug(st.logger, "Writer task: cancelled (1)."); } else { log_err(st.logger, "Error writing data to the server: ", ec); @@ -107,7 +106,7 @@ writer_action writer_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Writer task: cancelled (2)."); - return system::error_code(asio::error::operation_aborted); + return make_error_code(system::errc::operation_canceled); } // If we weren't notified, it's because there is no data and we should send a health check diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index ecc94d5be..3712ea9fb 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -4,25 +4,6 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +// Retained for backwards-compatibility (TODO: test) +#include +#include diff --git a/include/boost/redis/src/asio.hpp b/include/boost/redis/src/asio.hpp new file mode 100644 index 000000000..5f68727fd --- /dev/null +++ b/include/boost/redis/src/asio.hpp @@ -0,0 +1,8 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include diff --git a/include/boost/redis/src/corosio.hpp b/include/boost/redis/src/corosio.hpp new file mode 100644 index 000000000..4739762d3 --- /dev/null +++ b/include/boost/redis/src/corosio.hpp @@ -0,0 +1,8 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include diff --git a/include/boost/redis/src/proto.hpp b/include/boost/redis/src/proto.hpp new file mode 100644 index 000000000..004a4f60e --- /dev/null +++ b/include/boost/redis/src/proto.hpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018-2025 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aabf8b7b2..dfb0bddb9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,75 +10,97 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL target_compile_options(boost_redis_project_options INTERFACE -Wall -Wextra -Werror) endif() -add_library(boost_redis_src STATIC boost_redis.cpp) -target_link_libraries(boost_redis_src PRIVATE boost_redis_project_options) +# Source libraries +add_library(boost_redis_proto STATIC boost_redis_proto.cpp) +target_link_libraries(boost_redis_proto PUBLIC boost_redis_project_options) + +add_library(boost_redis_asio STATIC boost_redis_asio.cpp) +target_link_libraries(boost_redis_asio PUBLIC boost_redis_proto) + +add_library(boost_redis_corosio STATIC boost_redis_corosio.cpp) +target_link_libraries(boost_redis_corosio PUBLIC boost_redis_proto) # Test utils -add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp) -target_link_libraries(boost_redis_tests_common PRIVATE boost_redis_project_options) +add_library(boost_redis_tests_proto STATIC common.cpp sansio_utils.cpp) +target_link_libraries(boost_redis_tests_proto PUBLIC boost_redis_proto) +target_compile_definitions(boost_redis_tests_proto PUBLIC BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns + +add_library(boost_redis_tests_asio STATIC asio_common.cpp) +target_link_libraries(boost_redis_tests_asio PUBLIC boost_redis_asio boost_redis_tests_proto) -macro(make_test TEST_NAME) +add_library(boost_redis_tests_corosio STATIC corosio_common.cpp) +target_link_libraries(boost_redis_tests_corosio PUBLIC boost_redis_corosio boost_redis_tests_proto) + +function(make_test TEST_NAME) set(EXE_NAME "boost_redis_${TEST_NAME}") add_executable(${EXE_NAME} ${TEST_NAME}.cpp) - target_link_libraries(${EXE_NAME} PRIVATE - boost_redis_src - boost_redis_tests_common - boost_redis_project_options - Boost::unit_test_framework - ) + if (ARGN) + target_link_libraries(${EXE_NAME} PRIVATE ${ARGN}) + endif() + target_compile_definitions(${EXE_NAME} PRIVATE BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns - add_test(${EXE_NAME} ${EXE_NAME}) -endmacro() + add_test(NAME ${EXE_NAME} COMMAND ${EXE_NAME}) +endfunction() # Unit tests -make_test(test_low_level) -make_test(test_request) -make_test(test_serialization) -make_test(test_low_level_sync_sans_io) -make_test(test_any_adapter) -make_test(test_log_to_file) -make_test(test_conn_logging) -make_test(test_exec_fsm) -make_test(test_exec_one_fsm) -make_test(test_writer_fsm) -make_test(test_reader_fsm) -make_test(test_connect_fsm) -make_test(test_sentinel_resolve_fsm) -make_test(test_receive_fsm) -make_test(test_run_fsm) -make_test(test_compose_setup_request) -make_test(test_setup_adapter) -make_test(test_multiplexer) -make_test(test_parse_sentinel_response) -make_test(test_update_sentinel_list) -make_test(test_flat_tree) -make_test(test_generic_flat_response) -make_test(test_read_buffer) -make_test(test_subscription_tracker) -make_test(test_push_parser) +make_test(test_low_level boost_redis_tests_proto) +make_test(test_request boost_redis_tests_proto) +make_test(test_serialization boost_redis_tests_proto) +make_test(test_low_level_sync_sans_io boost_redis_tests_proto) +make_test(test_any_adapter boost_redis_tests_proto) +make_test(test_log_to_file boost_redis_tests_proto) +make_test(test_conn_logging boost_redis_tests_asio) +make_test(test_co_logging boost_redis_tests_corosio) +make_test(test_exec_fsm boost_redis_tests_proto) +make_test(test_exec_one_fsm boost_redis_tests_proto) +make_test(test_writer_fsm boost_redis_tests_proto) +make_test(test_reader_fsm boost_redis_tests_proto) +make_test(test_connect_fsm boost_redis_tests_asio) +make_test(test_sentinel_resolve_fsm boost_redis_tests_proto) +make_test(test_receive_fsm boost_redis_tests_proto) +make_test(test_run_fsm boost_redis_tests_proto) +make_test(test_compose_setup_request boost_redis_tests_proto) +make_test(test_setup_adapter boost_redis_tests_proto) +make_test(test_multiplexer boost_redis_tests_proto) +make_test(test_parse_sentinel_response boost_redis_tests_proto) +make_test(test_update_sentinel_list boost_redis_tests_proto) +make_test(test_flat_tree boost_redis_tests_proto) +make_test(test_generic_flat_response boost_redis_tests_proto) +make_test(test_read_buffer boost_redis_tests_proto) +make_test(test_subscription_tracker boost_redis_tests_proto) +make_test(test_push_parser boost_redis_tests_proto) # Tests that require a real Redis server -make_test(test_conn_quit) -make_test(test_conn_exec_retry) -make_test(test_conn_exec_error) -make_test(test_run) -make_test(test_conn_run_cancel) -make_test(test_conn_check_health) -make_test(test_conn_exec) -make_test(test_conn_push) -make_test(test_conn_push2) -make_test(test_conn_monitor) -make_test(test_conn_reconnect) -make_test(test_conn_exec_cancel) -make_test(test_conn_echo_stress) -make_test(test_conn_move) -make_test(test_conn_setup) -make_test(test_issue_50) -make_test(test_conversions) -make_test(test_conn_tls) -make_test(test_unix_sockets) -make_test(test_conn_cancel_after) -make_test(test_conn_sentinel) +make_test(test_conn_quit boost_redis_tests_asio) +make_test(test_conn_exec_retry boost_redis_tests_asio) +make_test(test_conn_exec_error boost_redis_tests_asio) +make_test(test_run boost_redis_tests_asio) +make_test(test_conn_run_cancel boost_redis_tests_asio) +make_test(test_co_run_cancel boost_redis_tests_corosio) +make_test(test_conn_check_health boost_redis_tests_asio) +make_test(test_co_check_health boost_redis_tests_corosio) +make_test(test_conn_exec boost_redis_tests_asio) +make_test(test_conn_push boost_redis_tests_asio) +make_test(test_conn_push2 boost_redis_tests_asio) +make_test(test_co_push2 boost_redis_tests_corosio) +make_test(test_conn_monitor boost_redis_tests_asio) +make_test(test_conn_reconnect boost_redis_tests_asio) +make_test(test_conn_exec_cancel boost_redis_tests_asio) +make_test(test_co_exec_cancel boost_redis_tests_corosio) +make_test(test_conn_echo_stress boost_redis_tests_asio) +make_test(test_conn_move boost_redis_tests_asio) +make_test(test_co_move boost_redis_tests_corosio) +make_test(test_conn_setup boost_redis_tests_asio) +make_test(test_co_setup boost_redis_tests_corosio) +make_test(test_issue_50 boost_redis_tests_asio) +make_test(test_conversions boost_redis_tests_asio) +make_test(test_conn_tls boost_redis_tests_asio) +make_test(test_co_tls boost_redis_tests_corosio) +make_test(test_unix_sockets boost_redis_tests_asio) +make_test(test_co_unix_sockets boost_redis_tests_corosio) +make_test(test_conn_cancel_after boost_redis_tests_asio) +make_test(test_conn_sentinel boost_redis_tests_asio) +make_test(test_co_sentinel boost_redis_tests_corosio) # Coverage set( diff --git a/test/asio_common.cpp b/test/asio_common.cpp new file mode 100644 index 000000000..70e56b1a3 --- /dev/null +++ b/test/asio_common.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include +#include +#include + +#include "asio_common.hpp" +#include "common.hpp" + +#include +#include +#include +#include +#include + +namespace net = boost::asio; + +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; + 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}); +} + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +void run_coroutine_test(net::awaitable op, std::chrono::steady_clock::duration timeout) +{ + net::io_context ioc; + bool finished = false; + net::co_spawn(ioc, std::move(op), [&finished](std::exception_ptr p) { + if (p) + std::rethrow_exception(p); + finished = true; + }); + ioc.run_for(timeout); + if (!finished) + throw std::runtime_error("Coroutine test did not finish"); +} +#endif // BOOST_ASIO_HAS_CO_AWAIT + +void create_user(std::string_view port, std::string_view username, std::string_view password) +{ + // Setup + net::io_context ioc; + boost::redis::connection conn{ioc}; + + boost::redis::config cfg; + cfg.addr.port = port; + + // Enable the user and grant them permissions on everything + boost::redis::request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + bool run_finished = false, exec_finished = false; + + conn.async_run(cfg, [&](boost::system::error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, boost::system::error_code()); + conn.cancel(); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); + BOOST_TEST(exec_finished); +} diff --git a/test/asio_common.hpp b/test/asio_common.hpp new file mode 100644 index 000000000..94e04d9a0 --- /dev/null +++ b/test/asio_common.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +inline auto redir(boost::system::error_code& ec) +{ + return boost::asio::redirect_error(boost::asio::use_awaitable, ec); +} +void run_coroutine_test( + boost::asio::awaitable, + std::chrono::steady_clock::duration timeout = test_timeout); +#endif // BOOST_ASIO_HAS_CO_AWAIT + +void run( + std::shared_ptr conn, + boost::redis::config cfg = make_test_config(), + boost::system::error_code ec = boost::asio::error::operation_aborted, + boost::redis::operation op = boost::redis::operation::receive); + +// Connects to the Redis server at the given port and creates a user +void create_user(std::string_view port, std::string_view username, std::string_view password); diff --git a/test/boost_redis.cpp b/test/boost_redis_asio.cpp similarity index 83% rename from test/boost_redis.cpp rename to test/boost_redis_asio.cpp index dddc80f2c..9c0bff144 100644 --- a/test/boost_redis.cpp +++ b/test/boost_redis_asio.cpp @@ -4,4 +4,4 @@ * accompanying file LICENSE.txt) */ -#include +#include diff --git a/test/boost_redis_corosio.cpp b/test/boost_redis_corosio.cpp new file mode 100644 index 000000000..395cd8777 --- /dev/null +++ b/test/boost_redis_corosio.cpp @@ -0,0 +1,7 @@ +/* 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 diff --git a/test/boost_redis_proto.cpp b/test/boost_redis_proto.cpp new file mode 100644 index 000000000..2c76307d7 --- /dev/null +++ b/test/boost_redis_proto.cpp @@ -0,0 +1,7 @@ +/* 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 diff --git a/test/common.cpp b/test/common.cpp index e13de33a6..aa9f4d172 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,42 +1,17 @@ #include -#include -#include -#include +#include #include #include "common.hpp" #include #include -#include -#include +#include #include -namespace net = boost::asio; using namespace std::chrono_literals; -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; - 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}); -} - std::string safe_getenv(const char* name, const char* default_value) { // MSVC doesn't like getenv @@ -61,22 +36,6 @@ boost::redis::config make_test_config() return cfg; } -#ifdef BOOST_ASIO_HAS_CO_AWAIT -void run_coroutine_test(net::awaitable op, std::chrono::steady_clock::duration timeout) -{ - net::io_context ioc; - bool finished = false; - net::co_spawn(ioc, std::move(op), [&finished](std::exception_ptr p) { - if (p) - std::rethrow_exception(p); - finished = true; - }); - ioc.run_for(timeout); - if (!finished) - throw std::runtime_error("Coroutine test did not finish"); -} -#endif // BOOST_ASIO_HAS_CO_AWAIT - // Finds a value in the output of the CLIENT INFO command // format: key1=value1 key2=value2 std::string_view find_client_info(std::string_view client_info, std::string_view key) @@ -92,38 +51,6 @@ std::string_view find_client_info(std::string_view client_info, std::string_view return client_info.substr(pos_begin, pos_end - pos_begin); } -void create_user(std::string_view port, std::string_view username, std::string_view password) -{ - // Setup - net::io_context ioc; - boost::redis::connection conn{ioc}; - - boost::redis::config cfg; - cfg.addr.port = port; - - // Enable the user and grant them permissions on everything - boost::redis::request req; - req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - - bool run_finished = false, exec_finished = false; - - conn.async_run(cfg, [&](boost::system::error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); - - conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, boost::system::error_code()); - conn.cancel(); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); - BOOST_TEST(exec_finished); -} - boost::redis::logger make_string_logger(std::string& to) { return { @@ -133,3 +60,13 @@ boost::redis::logger make_string_logger(std::string& to) to += '\n'; }}; } + +std::ostream& operator<<(std::ostream& os, const condition_wrapper& val) +{ + return os << val.value.category().name() << ':' << val.value.value() << " (" + << val.value.message() << ')'; +} + +// TODO: this should use std::errc::operation_canceled +// https://github.com/cppalliance/capy/issues/267 +condition_wrapper canceled_condition() { return {boost::capy::cond::canceled}; } \ No newline at end of file diff --git a/test/common.hpp b/test/common.hpp index 1dafbe260..cfa64b2bc 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -1,51 +1,57 @@ #pragma once -#include -#include +#include #include -#include -#include -#include -#include +#include #include +#include #include -#include +#include #include #include +#include // The timeout for tests involving communication to a real server. // Some tests use a longer timeout by multiplying this value by some // integral number. inline constexpr std::chrono::seconds test_timeout{30}; -#ifdef BOOST_ASIO_HAS_CO_AWAIT -inline auto redir(boost::system::error_code& ec) -{ - return boost::asio::redirect_error(boost::asio::use_awaitable, ec); -} -void run_coroutine_test( - boost::asio::awaitable, - std::chrono::steady_clock::duration timeout = test_timeout); -#endif // BOOST_ASIO_HAS_CO_AWAIT - boost::redis::config make_test_config(); std::string get_server_hostname(); -void run( - std::shared_ptr conn, - boost::redis::config cfg = make_test_config(), - boost::system::error_code ec = boost::asio::error::operation_aborted, - boost::redis::operation op = boost::redis::operation::receive); - // Finds a value in the output of the CLIENT INFO command // format: key1=value1 key2=value2 std::string_view find_client_info(std::string_view client_info, std::string_view key); -// Connects to the Redis server at the given port and creates a user -void create_user(std::string_view port, std::string_view username, std::string_view password); - boost::redis::logger make_string_logger(std::string& to); std::string safe_getenv(const char* name, const char* default_value); + +// std::error_condition doesn't implement operator<< and is difficult to use in tests +struct condition_wrapper { + std::error_condition value; + + friend bool operator==(const condition_wrapper& lhs, const condition_wrapper& rhs) noexcept + { + return lhs.value == rhs.value; + } + + template + friend bool operator==(const T& lhs, const condition_wrapper& rhs) noexcept + { + return lhs == rhs.value; + } + + template + friend bool operator==(const condition_wrapper& lhs, const T& rhs) noexcept + { + return lhs.value == rhs; + } + + friend std::ostream& operator<<(std::ostream& os, const condition_wrapper& val); +}; + +// Reduce verbosity in tests +condition_wrapper canceled_condition(); diff --git a/test/corosio_common.cpp b/test/corosio_common.cpp new file mode 100644 index 000000000..cafdb0590 --- /dev/null +++ b/test/corosio_common.cpp @@ -0,0 +1,77 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include + +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include + +namespace capy = boost::capy; + +void boost::redis::test::run_coroutine_test(capy::task test, source_location loc) +{ + // Set a timeout to the tests, so they don't hang on error + bool finished = false; + auto wrapper_fn = [test = std::move(test), &finished]() mutable -> capy::task { + co_await std::move(test); + finished = true; + }; + + // Actually run the test + corosio::io_context ctx; + capy::run_async(ctx.get_executor())(wrapper_fn()); + ctx.run_for(test_timeout); + + // Check that it finished + if (!BOOST_TEST(finished)) + std::cerr << " Called from " << loc << std::endl; +} + +capy::task boost::redis::test::create_user( + std::string_view port, + std::string_view username, + std::string_view password, + source_location loc) +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Enable the user and grant them permissions on everything + request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + auto [ec] = co_await conn.exec(req, ignore); + if (!BOOST_TEST_EQ(ec, std::error_code())) + std::cerr << " Called from " << loc << std::endl; + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + config cfg; + cfg.addr.port = port; + + auto [ec] = co_await conn.run(cfg); + if (!BOOST_TEST_EQ(ec, canceled_condition())) + std::cerr << " Called from " << loc << std::endl; + + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + if (!BOOST_TEST_EQ(result.index(), 1u)) // Exec finished 1st + std::cerr << " Called from " << loc << std::endl; +} diff --git a/test/corosio_common.hpp b/test/corosio_common.hpp new file mode 100644 index 000000000..fe1988b99 --- /dev/null +++ b/test/corosio_common.hpp @@ -0,0 +1,27 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_TEST_COROSIO_COMMON_HPP +#define BOOST_REDIS_TEST_COROSIO_COMMON_HPP + +#include +#include + +namespace boost::redis::test { + +void run_coroutine_test(capy::task test, source_location loc = BOOST_CURRENT_LOCATION); + +capy::task<> create_user( + std::string_view port, + std::string_view username, + std::string_view password, + source_location loc = BOOST_CURRENT_LOCATION); + +} // namespace boost::redis::test + +#endif diff --git a/test/test_co_check_health.cpp b/test/test_co_check_health.cpp new file mode 100644 index 000000000..c77017873 --- /dev/null +++ b/test/test_co_check_health.cpp @@ -0,0 +1,228 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using error_code = std::error_code; +using namespace std::chrono_literals; + +namespace { + +// The health checker detects dead connections and triggers reconnection +capy::task test_reconnection() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block forever, causing the connection to become unresponsive + request req1; + req1.push("BLPOP", "any", 0); + + // This request should be executed after reconnection + request req2; + req2.push("PING", "after_reconnection"); + req2.get_config().cancel_if_unresponded = false; + req2.get_config().cancel_on_connection_lost = false; + + // This request will complete after the health checker deems the connection + // as unresponsive and triggers a reconnection (it's configured to be cancelled + // on connection lost). + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, canceled_condition()); + + // Execute the second request. This one will succeed after reconnection + auto [ec2] = co_await conn.exec(req2, ignore); + BOOST_TEST_EQ(ec2, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// We use the correct error code when a ping times out +capy::task test_error_code() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block forever, causing the connection to become unresponsive + request req; + req.push("BLPOP", "any", 0); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 200ms; + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::pong_timeout); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// A ping interval of zero disables timeouts (and doesn't cause trouble) +capy::task test_disabled() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Run a couple of requests to verify that the connection works fine + request req1; + req1.push("PING", "health_check_disabled_1"); + + request req2; + req1.push("PING", "health_check_disabled_2"); + + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, std::error_code()); + + auto [ec2] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec2, std::error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// Generates a sufficiently unique name for channels so +// tests may be run in parallel for different configurations +std::string make_unique_id() +{ + auto t = std::chrono::high_resolution_clock::now(); + return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); +} + +// Receiving data is sufficient to consider our connection healthy. +// Sends a blocking request that causes PINGs to not be answered, +// and subscribes to a channel to receive pushes periodically. +// This simulates situations of heavy load, where PINGs may not be answered on time. +capy::task test_flexible() +{ + // Setup + co_connection conn1{co_await capy::this_coro::executor}; + co_connection conn2{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + std::string channel_name = make_unique_id(); + + auto run1_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn1.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto run2_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn2.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block for much longer than the health check + // interval. If we weren't receiving pushes, the connection would be considered dead. + // If this request finishes successfully, the health checker is doing good + request blocking_req; + blocking_req.push("SUBSCRIBE", channel_name); + blocking_req.push("BLPOP", "any", 2); + blocking_req.get_config().cancel_if_unresponded = true; + blocking_req.get_config().cancel_on_connection_lost = true; + + // BLPOP will return NIL, so we can't use ignore + generic_response resp; + auto [ec] = co_await conn1.exec(blocking_req, resp); + BOOST_TEST_EQ(ec, error_code()); + + co_return {}; + }; + + auto publish_fn = [&]() -> capy::io_task<> { + request publish_req; + publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); + + while (true) { + // Publish a message + auto [ec] = co_await conn2.exec(publish_req, ignore); + if (ec == canceled_condition()) + co_return {}; + BOOST_TEST_EQ(ec, error_code()); + + // Wait for some time and publish again + auto [ec2] = co_await capy::delay(100ms); + if (ec2 == canceled_condition()) + co_return {}; + BOOST_TEST_EQ(ec2, error_code()); + } + }; + + co_await capy::when_any(run1_fn(), run2_fn(), exec_fn(), publish_fn()); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_reconnection()); + run_coroutine_test(test_error_code()); + run_coroutine_test(test_disabled()); + run_coroutine_test(test_flexible()); + + return boost::report_errors(); +} \ No newline at end of file diff --git a/test/test_co_exec_cancel.cpp b/test/test_co_exec_cancel.cpp new file mode 100644 index 000000000..542120774 --- /dev/null +++ b/test/test_co_exec_cancel.cpp @@ -0,0 +1,108 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; + +namespace { + +// We can cancel requests that haven't been written yet +capy::task<> test_cancel_pending() +{ + co_connection conn{co_await capy::this_coro::executor}; + + // Issue a request without calling run, so the request stays waiting forever + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("get", "mykey"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), capy::ready()); + BOOST_TEST_EQ(result.index(), 2u); // Trigger finished 1st +} + +// We can cancel requests that have been written but whose +// responses haven't been received yet. +capy::task<> test_cancel_written() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Will be cancelled after it has been written but before the response arrives. + // Our BLPOP will block server-side for longer than the deadline below. + auto req1 = std::make_unique(); + req1->push("BLPOP", "any", 1); + auto resp1 = std::make_unique>(); + auto [ec1] = co_await capy::timeout(conn.exec(*req1, *resp1), 500ms); + BOOST_TEST_EQ(ec1, condition_wrapper{capy::cond::timeout}); + + // Destroy request and response to verify that we don't reference them after cancellation + req1.reset(); + resp1.reset(); + + // The connection remains usable. The PING's response will be received + // after the BLPOP's response, but it will be processed successfully. + request req2; + req2.push("PING", "after_blpop"); + response resp2; + auto [ec2] = co_await conn.exec(req2, resp2); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp2).value(), "after_blpop"); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_cancel_pending()); + run_coroutine_test(test_cancel_written()); + + return boost::report_errors(); +} diff --git a/test/test_co_logging.cpp b/test/test_co_logging.cpp new file mode 100644 index 000000000..e2708e7e8 --- /dev/null +++ b/test/test_co_logging.cpp @@ -0,0 +1,138 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "corosio_common.hpp" + +#include +#include +#include + +using boost::system::error_code; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; +using namespace boost::redis::test; + +namespace { + +config make_invalid_config() +{ + config cfg; + cfg.use_ssl = true; + cfg.unix_socket = "/tmp/sock"; + return cfg; +} + +struct fixture { + std::vector messages; + logger make_logger(logger::level lvl) + { + return {lvl, [this](logger::level, std::string_view msg) { + messages.emplace_back(msg); + }}; + } +}; + +capy::task<> test_connection_constructor_executor_1() +{ + // Setup + fixture fix; + co_connection conn{co_await capy::this_coro::executor, fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_context_1() +{ + // Setup + fixture fix; + auto ex = co_await capy::this_coro::executor; + co_connection conn{ex.context(), fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_executor_2() +{ + // Setup + fixture fix; + co_connection conn{ + co_await capy::this_coro::executor, + corosio::tls_context{}, + fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_context_2() +{ + // Setup + fixture fix; + auto ex = co_await capy::this_coro::executor; + co_connection conn{ex.context(), corosio::tls_context{}, fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_disable_logging() +{ + // Setup + fixture fix; + co_connection conn{co_await capy::this_coro::executor, fix.make_logger(logger::level::disabled)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // No logging should have been produced + BOOST_TEST_EQ(fix.messages.size(), 0u); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_connection_constructor_executor_1()); + run_coroutine_test(test_connection_constructor_executor_2()); + run_coroutine_test(test_connection_constructor_context_1()); + run_coroutine_test(test_connection_constructor_context_2()); + run_coroutine_test(test_disable_logging()); + + return boost::report_errors(); +} diff --git a/test/test_co_move.cpp b/test/test_co_move.cpp new file mode 100644 index 000000000..3ff0acf32 --- /dev/null +++ b/test/test_co_move.cpp @@ -0,0 +1,105 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; + +namespace { + +// Move constructing a connection doesn't leave dangling pointers +capy::task<> test_conn_move_construct() +{ + co_connection conn_prev{co_await capy::this_coro::executor}; + co_connection conn{std::move(conn_prev)}; + + response resp; + + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "something"); + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st + BOOST_TEST_EQ(std::get<0>(resp).value(), "something"); +} + +// Moving a connection is safe even when it's running, +// and it doesn't leave dangling pointers +capy::task<> test_conn_move_assign_while_running() +{ + co_connection conn{co_await capy::this_coro::executor}; + co_connection conn2{co_await capy::this_coro::executor}; // will be assigned to + + auto exec_fn = [&]() -> capy::io_task<> { + // Ensure that run is in flight + request req; + req.push("PING", "test_co_move"); + auto [ec] = co_await conn.exec(req); + BOOST_TEST_EQ(ec, error_code()); + + // Perform the move while run is in progress + conn2 = std::move(conn); + + // Checked that the moved-to connection is still usable + response resp; + auto [ec2] = co_await conn2.exec(req, resp); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "test_co_move"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_conn_move_construct()); + run_coroutine_test(test_conn_move_assign_while_running()); + + return boost::report_errors(); +} diff --git a/test/test_co_push2.cpp b/test/test_co_push2.cpp new file mode 100644 index 000000000..d24c1fdcb --- /dev/null +++ b/test/test_co_push2.cpp @@ -0,0 +1,664 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; +using resp3::flat_tree; +using resp3::node_view; +using resp3::type; + +// Covers all receive functionality for the new co_connection API. + +namespace { + +// receive() is outstanding when a push is received +capy::task<> test_receive_waiting_for_push() +{ + resp3::flat_tree resp; + co_connection conn{co_await capy::this_coro::executor}; + conn.set_receive_response(resp); + + auto exec1_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "Message1"); + req.push("SUBSCRIBE", "test_receive_waiting_for_push"); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto receive_then_exec2_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + + request req; + req.push("PING", "Message2"); + + auto [ec2] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec2, error_code()); + co_return {}; + }; + + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec1_fn(), receive_then_exec2_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st +} + +// A push is already available when receive() is called +capy::task<> test_receive_push_available() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + auto exec_fn = [&]() -> capy::io_task<> { + // SUBSCRIBE doesn't have a response, but causes a push to be delivered. + // Add a PING so the overall request has a response. + // This ensures that when exec completes, the push has been delivered + request req; + req.push("SUBSCRIBE", "test_receive_push_available"); + req.push("PING", "message"); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// receive() blocks only once if several messages are received in a batch +capy::task<> test_receive_batch() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + auto exec_fn = [&]() -> capy::io_task<> { + // 1. Trigger pushes + // This causes two messages to be delivered. The PING ensures that + // the pushes have been read when exec completes + request req; + req.push("SUBSCRIBE", "test_receive_batch"); + req.push("SUBSCRIBE", "test_receive_batch"); + req.push("PING", "message"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + + // 2. Receive both of them + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 2u); + + // 3. Check that receive has consumed them by calling it again with a deadline + auto [ec3] = co_await capy::timeout(conn.receive(), 50ms); + BOOST_TEST_EQ(ec3, condition_wrapper{capy::cond::timeout}); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// receive() can be called several times in a row +capy::task<> test_receive_subsequent_calls() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + auto exec_fn = [&]() -> capy::io_task<> { + // Send a SUBSCRIBE, which will trigger a push + request req; + req.push("SUBSCRIBE", "test_receive_subsequent_calls"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + + // Receive the push + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + resp.clear(); + + // Send another SUBSCRIBE, which will trigger another push + auto [ec3] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec3, error_code()); + + // Receive the push + auto [ec4] = co_await conn.receive(); + BOOST_TEST_EQ(ec4, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// receive() can be cancelled via stop token +capy::task<> test_receive_cancellation() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(receive_fn(), capy::ready()); + BOOST_TEST_EQ(result.index(), 2u); // trigger finished 1st +} + +// Reconnection doesn't cancel receive() +capy::task<> test_receive_reconnection() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + bool receive_finished = false; + + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, error_code()); + receive_finished = true; + co_return {}; + }; + + // Trigger a reconnection, then trigger a push to make receive complete + auto trigger_fn = [&]() -> capy::io_task<> { + // Causes a reconnection + request req_quit; + req_quit.push("QUIT"); + auto [ec_quit] = co_await conn.exec(req_quit, ignore); + static_cast(ec_quit); // QUIT may complete with success or an error; we don't care + + // Reconnection has happened by the time PING completes + request req_ping; + req_ping.get_config().cancel_if_unresponded = false; + req_ping.push("PING", "test_receive_reconnection"); + auto [ec_ping] = co_await conn.exec(req_ping, ignore); + BOOST_TEST_EQ(ec_ping, error_code()); + BOOST_TEST_NOT(receive_finished); + + // Generates a push + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "test_receive_reconnection"); + auto [ec_sub] = co_await conn.exec(req_subscribe, ignore); + BOOST_TEST_EQ(ec_sub, error_code()); + co_return {}; + }; + + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(receive_fn(), trigger_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st +} + +// A push may be interleaved between regular responses. +// It is handed to the receive adapter (filtered out). +capy::task<> test_exec_push_interleaved() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree receive_resp; + conn.set_receive_response(receive_resp); + + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "msg1"); + req.push("SUBSCRIBE", "test_exec_push_interleaved"); + req.push("PING", "msg2"); + + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "msg1"); + BOOST_TEST_EQ(std::get<1>(resp).value(), "msg2"); + co_return {}; + }; + + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(receive_resp.get_total_msgs(), 1u); + co_return {}; + }; + + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec_fn(), receive_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st +} + +// An adapter that always errors +struct response_error_tag { }; +response_error_tag error_tag_obj; + +struct response_error_adapter { + void on_init() { } + void on_done() { } + void on_node(node_view const&, error_code& ec) { ec = error::incompatible_size; } +}; + +auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; } + +// If the push adapter returns an error, the connection is torn down +capy::task<> test_push_adapter_error() +{ + co_connection conn{co_await capy::this_coro::executor}; + conn.set_receive_response(error_tag_obj); + + auto receive_fn = [&]() -> capy::io_task<> { + // Will be cancelled by when_any + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + // The request is cancelled because the PING response isn't processed + // by the time the error is generated + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.reconnect_wait_interval = 0s; // so we can validate the generated error + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::incompatible_size); + co_return {}; + }; + + co_await capy::when_any(receive_fn(), exec_fn(), run_fn()); +} + +// A push response error triggers a reconnection +capy::task<> test_push_adapter_error_reconnection() +{ + co_connection conn{co_await capy::this_coro::executor}; + conn.set_receive_response(error_tag_obj); + + // receive() will be cancelled by when_any + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto exec_fn = [&]() -> capy::io_task<> { + // The request is cancelled because the PING response isn't processed + // by the time the error is generated + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + + auto [ec1] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec1, canceled_condition()); + + // This one will succeed after reconnection + request req2; + req2.push("PING", "msg2"); + req2.get_config().cancel_if_unresponded = false; + + response resp; + + auto [ec2] = co_await conn.exec(req2, resp); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2"); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(receive_fn(), exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 2u); // exec finished after the receive cancel +} + +// Tests the usual push consumer pattern that we recommend in the examples +capy::task<> test_push_consumer() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + auto consumer_fn = [&]() -> capy::io_task<> { + while (true) { + auto [ec] = co_await conn.receive(); + resp.clear(); + if (ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + } + } + }; + + auto exec_fn = [&]() -> capy::io_task<> { + request req1; + req1.get_config().cancel_on_connection_lost = false; + req1.push("PING", "Message1"); + + request req2; + req2.get_config().cancel_on_connection_lost = false; + req2.push("SUBSCRIBE", "channel"); + + const request* sequence[] = + {&req1, &req2, &req2, &req1, &req2, &req1, &req2, &req2, &req1, &req2}; + for (const auto* r : sequence) { + auto [ec] = co_await conn.exec(*r, ignore); + BOOST_TEST_EQ(ec, error_code()); + } + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(consumer_fn(), exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 2u); // exec finished 1st +} + +// UNSUBSCRIBE and PUNSUBSCRIBE work +capy::task<> test_unsubscribe() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + response resp_subscribe, resp_unsubscribe, resp_ping; + + // Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "ch1", "ch2", "ch3"); + req_subscribe.push("PSUBSCRIBE", "ch1*", "ch2*"); + req_subscribe.push("CLIENT", "INFO"); + + auto [ec_sub] = co_await conn.exec(req_subscribe, resp_subscribe); + BOOST_TEST_EQ(ec_sub, error_code()); + BOOST_TEST(std::get<0>(resp_subscribe).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "psub"), "2"); + + // Then, unsubscribe from some of them, and verify again + request req_unsubscribe; + req_unsubscribe.push("UNSUBSCRIBE", "ch1"); + req_unsubscribe.push("PUNSUBSCRIBE", "ch2*"); + req_unsubscribe.push("CLIENT", "INFO"); + + auto [ec_unsub] = co_await conn.exec(req_unsubscribe, resp_unsubscribe); + BOOST_TEST_EQ(ec_unsub, error_code()); + BOOST_TEST(std::get<0>(resp_unsubscribe).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub"), "2"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub"), "1"); + + // Finally, ping to verify that the connection is still usable + request req_ping; + req_ping.push("PING", "test_unsubscribe"); + + auto [ec_ping] = co_await conn.exec(req_ping, resp_ping); + BOOST_TEST_EQ(ec_ping, error_code()); + BOOST_TEST(std::get<0>(resp_ping).has_value()); + BOOST_TEST_EQ(std::get<0>(resp_ping).value(), "test_unsubscribe"); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +void check_subscriptions(flat_tree const& resp_push) +{ + // Checks for the expected subscriptions and patterns after restoration + std::set seen_channels, seen_patterns; + for (auto it = resp_push.begin(); it != resp_push.end();) { + // The root element should be a push + BOOST_TEST_EQ(it->data_type, type::push); + BOOST_TEST_GE(it->aggregate_size, 2u); + BOOST_TEST(++it != resp_push.end()); + + // The next element should be the message type + std::string_view msg_type = it->value; + BOOST_TEST(++it != resp_push.end()); + + // The next element is the channel or pattern + if (msg_type == "subscribe") + seen_channels.insert(it->value); + else if (msg_type == "psubscribe") + seen_patterns.insert(it->value); + + // Skip the rest of the nodes + while (it != resp_push.end() && it->depth != 0u) + ++it; + } + + const std::string_view expected_channels[] = {"ch1", "ch3", "ch5"}; + const std::string_view expected_patterns[] = {"ch1*", "ch3*", "ch4*", "ch8*"}; + + BOOST_TEST_ALL_EQ( + seen_channels.begin(), + seen_channels.end(), + std::begin(expected_channels), + std::end(expected_channels)); + BOOST_TEST_ALL_EQ( + seen_patterns.begin(), + seen_patterns.end(), + std::begin(expected_patterns), + std::end(expected_patterns)); +} + +capy::task<> test_pubsub_state_restoration() +{ + co_connection conn{co_await capy::this_coro::executor}; + flat_tree resp_push; + conn.set_receive_response(resp_push); + + auto exec_fn = [&]() -> capy::io_task<> { + // Subscribe to some channels and patterns + request req1; + req1.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3 + req1.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4 + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, error_code()); + + // Unsubscribe from some channels and patterns. + // Unsubscribing from a channel/pattern that we weren't subscribed to is OK. + request req2; + req2.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3 + req2.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3 + auto [ec2] = co_await conn.exec(req2, ignore); + BOOST_TEST_EQ(ec2, error_code()); + + // Subscribe to other channels/patterns. + // Re-subscribing to channels/patterns we unsubscribed from is OK. + // Subscribing to the same channel/pattern twice is OK. + request req3; + req3.subscribe({"ch1", "ch3", "ch5"}); // active: 1, 3, 5 + req3.psubscribe({"ch3*", "ch4*", "ch8*"}); // active: 1, 3, 4, 8 + + // Subscriptions created by push() don't survive reconnection + req3.push("SUBSCRIBE", "ch10"); // active: 1, 3, 5, 10 + req3.push("PSUBSCRIBE", "ch10*"); // active: 1, 3, 4, 8, 10 + + // Validate that we're subscribed to what we expect + req3.push("CLIENT", "INFO"); + + response resp3; + + auto [ec3] = co_await conn.exec(req3, resp3); + BOOST_TEST_EQ(ec3, error_code()); + + // We are subscribed to 4 channels and 5 patterns + BOOST_TEST(std::get<0>(resp3).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp3).value(), "sub"), "4"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp3).value(), "psub"), "5"); + + resp_push.clear(); + + // Trigger a reconnection + request req4; + req4.push("QUIT"); + auto result = co_await conn.exec(req4, ignore); + static_cast(result); + // we don't know if this request will complete successfully or not + + // Verify state after reconnection + request req5; + req5.push("CLIENT", "INFO"); + req5.get_config().cancel_if_unresponded = false; + + response resp5; + + auto [ec5] = co_await conn.exec(req5, resp5); + BOOST_TEST_EQ(ec5, error_code()); + + // We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection) + BOOST_TEST(std::get<0>(resp5).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp5).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp5).value(), "psub"), "4"); + + // We have received pushes confirming it + check_subscriptions(resp_push); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_receive_waiting_for_push()); + run_coroutine_test(test_receive_push_available()); + run_coroutine_test(test_receive_batch()); + run_coroutine_test(test_receive_subsequent_calls()); + run_coroutine_test(test_receive_cancellation()); + run_coroutine_test(test_receive_reconnection()); + run_coroutine_test(test_exec_push_interleaved()); + run_coroutine_test(test_push_adapter_error()); + run_coroutine_test(test_push_adapter_error_reconnection()); + run_coroutine_test(test_push_consumer()); + run_coroutine_test(test_unsubscribe()); + run_coroutine_test(test_pubsub_state_restoration()); + + return boost::report_errors(); +} diff --git a/test/test_co_run_cancel.cpp b/test/test_co_run_cancel.cpp new file mode 100644 index 000000000..144fc779a --- /dev/null +++ b/test/test_co_run_cancel.cpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +using boost::system::error_code; +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; + +namespace { + +// The user configured an empty setup request. No request should be sent +capy::task<> test_cancel_run() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(config{}); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(run_fn(), capy::ready()); + BOOST_TEST_EQ(result.index(), 2u); // Ready finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_cancel_run()); + + return boost::report_errors(); +} diff --git a/test/test_co_sentinel.cpp b/test/test_co_sentinel.cpp new file mode 100644 index 000000000..1a08647e5 --- /dev/null +++ b/test/test_co_sentinel.cpp @@ -0,0 +1,425 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" +#include "print_node.hpp" + +#include +#include +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; + +namespace { + +config make_sentinel_config() +{ + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + return cfg; +} + +// We can execute requests normally when using Sentinel +capy::task<> test_exec() +{ + co_connection conn{co_await capy::this_coro::executor}; + + generic_response resp; + + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to the master + request req; + req.push("ROLE"); + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + + // ROLE outputs an array, 1st element should be 'master' + BOOST_TEST(resp.has_value()); + BOOST_TEST_GE(resp.value().size(), 2u); + BOOST_TEST_EQ(resp.value().at(1u).value, "master"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// We can use receive normally when using Sentinel +capy::task<> test_receive() +{ + co_connection conn{co_await capy::this_coro::executor}; + resp3::tree resp; + conn.set_receive_response(resp); + + auto exec_fn = [&]() -> capy::io_task<> { + // Subscribe to a channel. This produces a push message on itself + request req; + req.subscribe({"sentinel_channel"}); + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec_fn(), receive_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st + + // We subscribed to channel 'sentinel_channel', and have 1 active subscription + const resp3::node expected[] = { + {resp3::type::push, 3u, 0u, "" }, + {resp3::type::blob_string, 1u, 1u, "subscribe" }, + {resp3::type::blob_string, 1u, 1u, "sentinel_channel"}, + {resp3::type::number, 1u, 1u, "1" }, + }; + + BOOST_TEST_ALL_EQ(resp.begin(), resp.end(), std::begin(expected), std::end(expected)); +} + +// If connectivity to the Redis master fails, we can reconnect +capy::task<> test_reconnect() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Will cause the connection to fail + request req_quit; + req_quit.push("QUIT"); + auto [ec1] = co_await conn.exec(req_quit, ignore); + BOOST_TEST_EQ(ec1, error_code()); + + // Will succeed if the reconnection succeeds + request req_ping; + req_ping.push("PING", "sentinel_reconnect"); + req_ping.get_config().cancel_if_unresponded = false; + auto [ec2] = co_await conn.exec(req_ping, ignore); + BOOST_TEST_EQ(ec2, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// If a Sentinel is not reachable, we try the next one +capy::task<> test_sentinel_not_reachable() +{ + co_connection conn{co_await capy::this_coro::executor}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "45678"}, // invalid + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to the master + request req; + req.push("PING", "test_sentinel_not_reachable"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// Both Sentinels and masters may be protected with authorization +capy::task<> test_auth() +{ + co_connection conn{co_await capy::this_coro::executor}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_pass"); + + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", 3, "AUTH", "redis_user", "redis_pass"); + + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're authenticated correctly + request req; + req.push("ACL", "WHOAMI"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST(std::get<0>(resp).has_value()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "redis_user"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// TLS might be used with Sentinels. In our setup, nodes don't use TLS, +// but this setting is independent from Sentinel. +capy::task<> test_tls() +{ + // The custom server uses a certificate signed by a CA + // that is not trusted by default - skip verification. + corosio::tls_context tls_ctx; + tls_ctx.set_verify_mode(corosio::tls_verify_mode::none); + + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "36379"}, + {"localhost", "36380"}, + {"localhost", "36381"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.sentinel.use_ssl = true; + + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "test_sentinel_tls"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// We can also connect to replicas +capy::task<> test_replica() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to a replica + request req; + req.push("ROLE"); + generic_response resp; + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + + // ROLE outputs an array, 1st element should be 'slave' + BOOST_TEST(resp.has_value()); + BOOST_TEST_GE(resp.value().size(), 2u); + BOOST_TEST_EQ(resp.value().at(1u).value, "slave"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_sentinel_config(); + cfg.sentinel.server_role = role::replica; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// If no Sentinel is reachable, an error is issued. +// This tests disabling reconnection with Sentinel, too. +capy::task<> test_error_no_sentinel_reachable() +{ + std::string logs; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "43210"}, + {"localhost", "43211"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + + if ( + !BOOST_TEST_NE( + logs.find("Sentinel at localhost:43210: connection establishment error"), + std::string::npos) || + !BOOST_TEST_NE( + logs.find("Sentinel at localhost:43211: connection establishment error"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +// If Sentinel doesn't know about the configured master, +// the appropriate error is returned +capy::task<> test_error_unknown_master() +{ + std::string logs; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26380"}, + }; + cfg.sentinel.master_name = "unknown_master"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + + if (!BOOST_TEST_NE( + logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +// The same applies when connecting to replicas, too +capy::task<> test_error_unknown_master_replica() +{ + std::string logs; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26380"}, + }; + cfg.sentinel.master_name = "unknown_master"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + cfg.sentinel.server_role = role::replica; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + + if (!BOOST_TEST_NE( + logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +capy::task<> create_all_users() +{ + co_await create_user("6379", "redis_user", "redis_pass"); + co_await create_user("6380", "redis_user", "redis_pass"); + co_await create_user("6381", "redis_user", "redis_pass"); + co_await create_user("26379", "sentinel_user", "sentinel_pass"); + co_await create_user("26380", "sentinel_user", "sentinel_pass"); + co_await create_user("26381", "sentinel_user", "sentinel_pass"); +} + +} // namespace + +int main() +{ + // Create the required users in the master, replicas and sentinels + run_coroutine_test(create_all_users()); + + // Actual tests + run_coroutine_test(test_exec()); + run_coroutine_test(test_receive()); + run_coroutine_test(test_reconnect()); + run_coroutine_test(test_sentinel_not_reachable()); + run_coroutine_test(test_auth()); + run_coroutine_test(test_tls()); + run_coroutine_test(test_replica()); + + run_coroutine_test(test_error_no_sentinel_reachable()); + run_coroutine_test(test_error_unknown_master()); + run_coroutine_test(test_error_unknown_master_replica()); + + return boost::report_errors(); +} diff --git a/test/test_co_setup.cpp b/test/test_co_setup.cpp new file mode 100644 index 000000000..6398fe9a2 --- /dev/null +++ b/test/test_co_setup.cpp @@ -0,0 +1,270 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include + +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +namespace capy = boost::capy; +namespace corosio = boost::corosio; + +namespace { + +capy::task<> test_auth_success() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto request_fn = [&]() -> capy::io_task<> { + // This request should return the username we're logged in as + request req; + req.push("ACL", "WHOAMI"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // These credentials are set up in main, before tests are run + config cfg; + cfg.username = "myuser"; + cfg.password = "mypass"; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_auth_failure() +{ + // Setup + std::string logs; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.username = "myuser"; + cfg.password = "wrongpass"; // wrong + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { + std::cerr << "Log was: \n" << logs << std::endl; + } +} + +capy::task<> test_database_index() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto request_fn = [&]() -> capy::io_task<> { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // Use a non-default database index + auto cfg = make_test_config(); + cfg.database_index = 2; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// The user configured an empty setup request. No request should be sent +capy::task<> test_setup_empty() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto request_fn = [&]() -> capy::io_task<> { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// We can use the setup member to run commands at startup +capy::task<> test_setup_hello() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto request_fn = [&]() -> capy::io_task<> { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// Running a pipeline without a HELLO is okay (regression check: we set the priority flag) +capy::task<> test_setup_no_hello() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto request_fn = [&]() -> capy::io_task<> { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_setup_failure() +{ + // Setup + std::string logs; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +} // namespace + +int main() +{ + run_coroutine_test(create_user("6379", "myuser", "mypass")); + + run_coroutine_test(test_auth_success()); + run_coroutine_test(test_auth_failure()); + run_coroutine_test(test_database_index()); + run_coroutine_test(test_setup_empty()); + run_coroutine_test(test_setup_hello()); + run_coroutine_test(test_setup_no_hello()); + run_coroutine_test(test_setup_failure()); + + return boost::report_errors(); +} diff --git a/test/test_co_tls.cpp b/test/test_co_tls.cpp new file mode 100644 index 000000000..949cb71bb --- /dev/null +++ b/test/test_co_tls.cpp @@ -0,0 +1,173 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; + +namespace { + +// Loads the CA certificate that signed the certificate used by the server. +std::string load_ca_certificate() +{ + auto ca_path = safe_getenv("BOOST_REDIS_CA_PATH", "/opt/ci-tls/ca.crt"); + std::ifstream f(ca_path); + if (!f) { + throw boost::system::system_error( + errno, + boost::system::system_category(), + "Failed to open CA certificate file '" + ca_path + "'"); + } + + return std::string(std::istreambuf_iterator(f), std::istreambuf_iterator()); +} + +config make_tls_config() +{ + config cfg; + cfg.use_ssl = true; + cfg.addr.host = get_server_hostname(); + cfg.addr.port = "16379"; + return cfg; +} + +// Using the default TLS context (the one created if nothing is passed to the ctor) +// allows establishing TLS connections and execute requests +capy::task<> test_exec_default_tls_context() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "test_co_tls"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "test_co_tls"); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_tls_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// Users can pass a custom context with TLS config +capy::task<> test_exec_custom_ssl_context() +{ + // Configure the TLS context to trust the CA that signed the server's certificate. + // The test certificate uses "redis" as its common name, + // regardless of the actual server's hostname. + corosio::tls_context tls_ctx; + auto ec_ca = tls_ctx.add_certificate_authority(load_ca_certificate()); + BOOST_TEST_EQ(ec_ca, error_code()); + auto ec_mode = tls_ctx.set_verify_mode(corosio::tls_verify_mode::require_peer); + BOOST_TEST_EQ(ec_mode, error_code()); + tls_ctx.set_hostname("redis"); + + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; + + auto exec_fn = [&]() -> capy::io_task<> { + constexpr std::string_view ping_value = "Kabuf"; + request req; + req.push("PING", ping_value); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_tls_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +// After an error, a connection can recover. +// Force an error using QUIT, then issue a regular request to verify that we could reconnect. +capy::task<> test_reconnection() +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + request quit_request; + quit_request.push("QUIT"); + auto [ec_quit] = co_await conn.exec(quit_request, ignore); + BOOST_TEST_EQ(ec_quit, error_code()); + + request ping_request; + ping_request.push("PING", "some_value"); + ping_request.get_config().cancel_if_unresponded = false; + ping_request.get_config().cancel_on_connection_lost = false; + auto [ec_ping] = co_await conn.exec(ping_request, ignore); + BOOST_TEST_EQ(ec_ping, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_exec_default_tls_context()); + run_coroutine_test(test_exec_custom_ssl_context()); + run_coroutine_test(test_reconnection()); + + return boost::report_errors(); +} diff --git a/test/test_co_unix_sockets.cpp b/test/test_co_unix_sockets.cpp new file mode 100644 index 000000000..01132b6f4 --- /dev/null +++ b/test/test_co_unix_sockets.cpp @@ -0,0 +1,169 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using error_code = std::error_code; +using namespace std::string_view_literals; + +namespace { + +constexpr std::string_view unix_socket_path = "/tmp/redis-socks/redis.sock"; + +// Executing commands using UNIX sockets works +capy::task test_exec() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.unix_socket = unix_socket_path; + + request req; + req.push("PING", "unix"); + response res; + + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req, res); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(std::get<0>(res).value(), "unix"sv); +} + +// If the connection is lost when using a UNIX socket, we can reconnect +capy::task test_reconnection() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.unix_socket = unix_socket_path; + + request ping_request; + ping_request.get_config().cancel_if_not_connected = false; + ping_request.get_config().cancel_if_unresponded = false; + ping_request.get_config().cancel_on_connection_lost = false; + ping_request.push("PING", "some_value"); + + request quit_request; + quit_request.push("QUIT"); + + auto exec_fn = [&]() -> capy::io_task<> { + auto [quit_ec] = co_await conn.exec(quit_request, ignore); + BOOST_TEST_EQ(quit_ec, error_code()); + + auto [ping_ec] = co_await conn.exec(ping_request, ignore); + BOOST_TEST_EQ(ping_ec, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// We can freely switch between UNIX sockets and other transports +capy::task test_switch_between_transports() +{ + co_connection conn{co_await capy::this_coro::executor}; + request req; + req.push("PING", "hello"); + + // Create configurations for TLS and UNIX connections + auto tcp_tls_cfg = make_test_config(); + tcp_tls_cfg.use_ssl = true; + tcp_tls_cfg.addr.port = "16380"; + auto unix_cfg = make_test_config(); + unix_cfg.unix_socket = unix_socket_path; + + auto run_once = [&](const config& cfg) -> capy::task<> { + auto when_any_res = co_await capy::when_any( + [&]() -> capy::io_task<> { + response res; + auto [ec] = co_await conn.exec(req, res); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(res).value(), "hello"); + co_return {}; + }(), + [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }()); + BOOST_TEST_EQ(when_any_res.index(), 1); + }; + + // Run the connection with TCP/TLS + std::cerr << "test_switch_between_transports: TLS 1\n"; + co_await run_once(tcp_tls_cfg); + + // Switch to UNIX + std::cerr << "test_switch_between_transports: UNIX\n"; + co_await run_once(unix_cfg); + + // Go back to TCP/TLS + std::cerr << "test_switch_between_transports: TLS 2\n"; + co_await run_once(tcp_tls_cfg); +} + +// Trying to enable TLS and UNIX sockets at the same time +// is an error and makes run exit immediately +capy::task test_error_unix_tls() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.use_ssl = true; + cfg.addr.port = "16380"; + cfg.unix_socket = unix_socket_path; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_exec()); + run_coroutine_test(test_reconnection()); + run_coroutine_test(test_switch_between_transports()); + run_coroutine_test(test_error_unix_tls()); + + return boost::report_errors(); +} diff --git a/test/test_conn_cancel_after.cpp b/test/test_conn_cancel_after.cpp index b7f358005..9c293083f 100644 --- a/test/test_conn_cancel_after.cpp +++ b/test/test_conn_cancel_after.cpp @@ -40,7 +40,7 @@ void test_run() // Call the function with a very short timeout conn.async_run(make_test_config(), asio::cancel_after(1ms, [&](error_code ec) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; })); @@ -63,7 +63,7 @@ void test_exec() // Call the function with a very short timeout. // The connection is not being run, so these can't succeed conn.async_exec(req, ignore, asio::cancel_after(1ms, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec_finished = true; })); @@ -105,7 +105,7 @@ void test_receive2() // Call the function with a very short timeout. conn.async_receive2(asio::cancel_after(1ms, [&](error_code ec) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; })); diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 5d53b0844..f66ae2e74 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -56,7 +56,7 @@ void test_reconnection() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // This request will complete after the health checker deems the connection @@ -64,7 +64,7 @@ void test_reconnection() // on connection lost). conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { exec1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); // Execute the second request. This one will succeed after reconnection conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { @@ -109,7 +109,7 @@ void test_error_code() // if unresponded). conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { exec_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -139,7 +139,7 @@ void test_disabled() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { @@ -225,12 +225,12 @@ class test_flexible { conn1.async_run(cfg, [&](error_code ec) { run1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); conn2.async_run(cfg, [&](error_code ec) { run2_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // BLPOP will return NIL, so we can't use ignore diff --git a/test/test_conn_echo_stress.cpp b/test/test_conn_echo_stress.cpp index e1455b2aa..0dfa065d3 100644 --- a/test/test_conn_echo_stress.cpp +++ b/test/test_conn_echo_stress.cpp @@ -121,7 +121,7 @@ BOOST_AUTO_TEST_CASE(echo_stress) bool run_finished = false, subscribe_finished = false; conn.async_run(cfg, logger{logger::level::crit}, [&run_finished](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); std::clog << "async_run finished" << std::endl; }); diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index 715d844fa..1b472e43b 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -15,7 +15,7 @@ #define BOOST_TEST_MODULE conn_exec #include -#include "common.hpp" +#include "asio_common.hpp" #include diff --git a/test/test_conn_exec_cancel.cpp b/test/test_conn_exec_cancel.cpp index 939cd3617..0bb49255c 100644 --- a/test/test_conn_exec_cancel.cpp +++ b/test/test_conn_exec_cancel.cpp @@ -70,7 +70,7 @@ void test_cancel_pending() req, ignore, net::bind_cancellation_slot(sig.slot(), [&](error_code ec, std::size_t sz) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); BOOST_TEST_EQ(sz, 0u); called = true; })); @@ -116,7 +116,7 @@ void test_cancel_written() // Run the connection conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -127,14 +127,14 @@ void test_cancel_written() auto blpop_cb = [&](error_code ec, std::size_t) { req1.reset(); r1.reset(); - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec1_finished = true; }; conn.async_exec(*req1, *r1, net::cancel_after(500ms, blpop_cb)); // The first PING will be cancelled, too. Use partial cancellation here. auto req2_cb = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec2_finished = true; }; conn.async_exec( @@ -203,7 +203,7 @@ void test_cancel_on_connection_lost_written() // Run the connection auto cfg = make_test_config(); conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -219,7 +219,7 @@ void test_cancel_on_connection_lost_written() }); conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec1_finished = true; }); @@ -251,7 +251,7 @@ void test_cancel_operation_exec() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_exec_error.cpp b/test/test_conn_exec_error.cpp index b77f12997..09c53284e 100644 --- a/test/test_conn_exec_error.cpp +++ b/test/test_conn_exec_error.cpp @@ -9,7 +9,7 @@ #define BOOST_TEST_MODULE conn_exec_error #include -#include "common.hpp" +#include "asio_common.hpp" #include #include @@ -308,7 +308,7 @@ BOOST_AUTO_TEST_CASE(issue_287_generic_response_error_then_success) bool run_finished = false, exec_finished = false; conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_exec_retry.cpp b/test/test_conn_exec_retry.cpp index e9d941c56..ff3d9f214 100644 --- a/test/test_conn_exec_retry.cpp +++ b/test/test_conn_exec_retry.cpp @@ -70,13 +70,13 @@ BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_true) auto c2 = [&](error_code ec, std::size_t) { c2_called = true; std::cout << "c2" << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c1 = [&](error_code ec, std::size_t) { c1_called = true; std::cout << "c1" << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c0 = [&](error_code ec, std::size_t) { @@ -150,7 +150,7 @@ BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_false) auto c1 = [&](error_code ec, std::size_t) { c1_called = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c0 = [&](error_code ec, std::size_t) { diff --git a/test/test_conn_monitor.cpp b/test/test_conn_monitor.cpp index 090c54557..0feaac013 100644 --- a/test/test_conn_monitor.cpp +++ b/test/test_conn_monitor.cpp @@ -91,7 +91,7 @@ class test_monitor { // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Issue the monitor, then start generating traffic diff --git a/test/test_conn_move.cpp b/test/test_conn_move.cpp index e7726d6f4..56025faa8 100644 --- a/test/test_conn_move.cpp +++ b/test/test_conn_move.cpp @@ -43,7 +43,7 @@ void test_conn_move_construct() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Launch a PING @@ -78,7 +78,7 @@ void test_conn_move_assign_while_running() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Launch a PING. When it finishes, conn will be moved-from, and conn2 will be valid diff --git a/test/test_conn_push.cpp b/test/test_conn_push.cpp index 0ba3258f8..b40f8995a 100644 --- a/test/test_conn_push.cpp +++ b/test/test_conn_push.cpp @@ -68,7 +68,7 @@ void test_async_receive_waiting_for_push() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -112,7 +112,7 @@ void test_async_receive_push_available() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -165,7 +165,7 @@ void test_sync_receive() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -266,7 +266,7 @@ struct test_async_receive_cancelled_on_reconnection_impl { conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -361,7 +361,7 @@ void test_consecutive_receives() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_push2.cpp b/test/test_conn_push2.cpp index c76191ddc..495e742d8 100644 --- a/test/test_conn_push2.cpp +++ b/test/test_conn_push2.cpp @@ -79,7 +79,7 @@ void test_async_receive2_waiting_for_push() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -123,7 +123,7 @@ void test_async_receive2_push_available() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -155,7 +155,7 @@ void test_async_receive2_batch() // 2. Receive both of them // 3. Check that receive2 has consumed them by calling it again auto on_receive2 = [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; conn.cancel(); }; @@ -173,7 +173,7 @@ void test_async_receive2_batch() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -241,7 +241,7 @@ void test_async_receive2_subsequent_calls() start_subscribe1(); conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -267,7 +267,7 @@ void test_async_receive2_per_operation_cancellation( bool receive_finished = false; conn.async_receive2(net::bind_cancellation_slot(sig.slot(), [&](error_code ec) { - if (!BOOST_TEST_EQ(ec, net::error::operation_aborted)) + if (!BOOST_TEST_EQ(ec, canceled_condition())) std::cerr << "With cancellation type " << name << std::endl; receive_finished = true; })); @@ -290,7 +290,7 @@ void test_async_receive2_connection_cancel() bool receive_finished = false; conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; }); @@ -354,7 +354,7 @@ void test_async_receive2_reconnection() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -398,7 +398,7 @@ void test_exec_push_interleaved() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -436,14 +436,14 @@ void test_push_adapter_error() // We cancel receive when run exits conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; }); // The request is cancelled because the PING response isn't processed // by the time the error is generated conn.async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec_finished = true; }); @@ -483,7 +483,7 @@ void test_push_adapter_error_reconnection() // async_receive2 is cancelled every reconnection cycle conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); push_received = true; }); @@ -497,12 +497,12 @@ void test_push_adapter_error_reconnection() // The request is cancelled because the PING response isn't processed // by the time the error is generated conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); conn.async_exec(req2, resp, on_exec2); }); conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -524,7 +524,7 @@ void test_push_consumer() std::function launch_push_consumer = [&]() { conn.async_receive2([&](error_code ec) { if (ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); push_consumer_finished = true; resp.clear(); return; @@ -592,7 +592,7 @@ void test_push_consumer() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -658,7 +658,7 @@ void test_unsubscribe() conn.async_exec(req_subscribe, resp_subscribe, on_subscribe); conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -816,7 +816,7 @@ struct test_pubsub_state_restoration_impl { // Start running bool run_finished = false; conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_quit.cpp b/test/test_conn_quit.cpp index 5a614a379..cd70ebfd2 100644 --- a/test/test_conn_quit.cpp +++ b/test/test_conn_quit.cpp @@ -12,7 +12,7 @@ #define BOOST_TEST_MODULE conn_quit #include -#include "common.hpp" +#include "asio_common.hpp" #include @@ -51,7 +51,7 @@ BOOST_AUTO_TEST_CASE(test_async_run_exits) auto c3 = [&](error_code ec, std::size_t) { c3_called = true; std::clog << "c3: " << ec.message() << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c2 = [&](error_code ec, std::size_t) { diff --git a/test/test_conn_reconnect.cpp b/test/test_conn_reconnect.cpp index 7740c9b55..7c58745b1 100644 --- a/test/test_conn_reconnect.cpp +++ b/test/test_conn_reconnect.cpp @@ -9,10 +9,12 @@ #include +#include "common.hpp" + #define BOOST_TEST_MODULE conn_reconnect #include -#include "common.hpp" +#include "asio_common.hpp" #include @@ -109,7 +111,7 @@ auto async_test_reconnect_timeout() -> net::awaitable std::cout << "ccc" << std::endl; - BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); + BOOST_CHECK_EQUAL(ec1, canceled_condition()); } BOOST_AUTO_TEST_CASE(test_reconnect_and_idle) diff --git a/test/test_conn_run_cancel.cpp b/test/test_conn_run_cancel.cpp index 053c58f59..5f1fe8aed 100644 --- a/test/test_conn_run_cancel.cpp +++ b/test/test_conn_run_cancel.cpp @@ -46,7 +46,7 @@ void test_per_operation_cancellation(std::string_view name, net::cancellation_ty // Run the connection auto run_cb = [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; conn.async_run(make_test_config(), net::bind_cancellation_slot(sig.slot(), run_cb)); diff --git a/test/test_conn_sentinel.cpp b/test/test_conn_sentinel.cpp index f18c1de08..782c66e96 100644 --- a/test/test_conn_sentinel.cpp +++ b/test/test_conn_sentinel.cpp @@ -18,7 +18,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include "print_node.hpp" #include @@ -67,7 +67,7 @@ void test_exec() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -113,7 +113,7 @@ void test_receive() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -171,7 +171,7 @@ void test_reconnect() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -209,7 +209,7 @@ void test_sentinel_not_reachable() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -254,7 +254,7 @@ void test_auth() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -299,7 +299,7 @@ void test_tls() conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -346,7 +346,7 @@ void test_replica() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 9997cb6b9..1eaa7139e 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -15,7 +15,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include #include @@ -54,7 +54,7 @@ void test_auth_success() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_tls.cpp b/test/test_conn_tls.cpp index 9e588884e..aa4383863 100644 --- a/test/test_conn_tls.cpp +++ b/test/test_conn_tls.cpp @@ -82,7 +82,7 @@ BOOST_AUTO_TEST_CASE(exec_default_ssl_context) conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -125,7 +125,7 @@ BOOST_AUTO_TEST_CASE(exec_custom_ssl_context) conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(reconnection) // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); // The PING is the end of the callback chain diff --git a/test/test_conversions.cpp b/test/test_conversions.cpp index ac6845066..53667a8a6 100644 --- a/test/test_conversions.cpp +++ b/test/test_conversions.cpp @@ -12,7 +12,7 @@ #define BOOST_TEST_MODULE conversions #include -#include "common.hpp" +#include "asio_common.hpp" namespace net = boost::asio; using boost::redis::connection; diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index 16301554d..99711d1ec 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -6,15 +6,17 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include +#include #include -#include #include #include #include +#include #include #include "sansio_utils.hpp" @@ -26,6 +28,8 @@ using namespace boost::redis; namespace asio = boost::asio; +namespace errc = boost::system::errc; +using detail::cancellation_type; using detail::exec_fsm; using detail::multiplexer; using detail::exec_action_type; @@ -33,7 +37,6 @@ using detail::consume_result; using detail::exec_action; using detail::connection_state; using boost::system::error_code; -using boost::asio::cancellation_type_t; #define BOOST_REDIS_EXEC_SWITCH_CASE(elem) \ case exec_action_type::elem: return "exec_action_type::" #elem @@ -141,13 +144,13 @@ void test_success() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -163,7 +166,7 @@ void test_success() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -180,13 +183,13 @@ void test_parse_error() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -204,7 +207,7 @@ void test_parse_error() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error::empty_field, 0u)); // All memory should have been freed by now @@ -222,10 +225,10 @@ void test_cancel_if_not_connected() exec_fsm fsm(std::move(input.elm)); // Initiate. We're not connected, so the request gets cancelled - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::immediate); - act = fsm.resume(false, st, cancellation_type_t::none); + act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error::not_connected)); // We didn't leave memory behind @@ -242,13 +245,13 @@ void test_not_connected() error_code ec; // Initiate - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -264,7 +267,7 @@ void test_not_connected() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -280,13 +283,13 @@ void test_cancel_waiting() { constexpr struct { const char* name; - asio::cancellation_type_t type; + cancellation_type type; } test_cases[] = { - {"terminal", asio::cancellation_type_t::terminal }, - {"partial", asio::cancellation_type_t::partial }, - {"total", asio::cancellation_type_t::total }, - {"mixed", asio::cancellation_type_t::partial | asio::cancellation_type_t::terminal}, - {"all", asio::cancellation_type_t::all }, + {"terminal", cancellation_type::terminal }, + {"partial", cancellation_type::partial }, + {"total", cancellation_type::total }, + {"mixed", cancellation_type::partial | cancellation_type::terminal }, + {"all", cancellation_type::terminal | cancellation_type::partial | cancellation_type::total}, }; for (const auto& tc : test_cases) { @@ -300,16 +303,16 @@ void test_cancel_waiting() BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name); // Initiate and wait - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name); // We get notified because the request got cancelled act = fsm.resume(true, st, tc.type); - BOOST_TEST_EQ_MSG(act, exec_action(asio::error::operation_aborted), tc.name); + BOOST_TEST_EQ_MSG(act, make_error_code(errc::operation_canceled), tc.name); BOOST_TEST_EQ_MSG(input.weak_elm.expired(), true, tc.name); // we didn't leave memory behind } } @@ -320,10 +323,10 @@ void test_cancel_notwaiting_terminal_partial() { constexpr struct { const char* name; - asio::cancellation_type_t type; + cancellation_type type; } test_cases[] = { - {"terminal", asio::cancellation_type_t::terminal}, - {"partial", asio::cancellation_type_t::partial }, + {"terminal", cancellation_type::terminal}, + {"partial", cancellation_type::partial }, }; for (const auto& tc : test_cases) { @@ -333,12 +336,12 @@ void test_cancel_notwaiting_terminal_partial() exec_fsm fsm(std::move(input->elm)); // Initiate - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name); // The multiplexer starts writing the request @@ -347,7 +350,7 @@ void test_cancel_notwaiting_terminal_partial() // A cancellation arrives act = fsm.resume(true, st, tc.type); - BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted)); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); input.reset(); // Verify we don't access the request or response after completion error_code ec; @@ -372,12 +375,12 @@ void test_cancel_notwaiting_total() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -385,7 +388,7 @@ void test_cancel_notwaiting_total() BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); // We got requested a cancellation here, but we can't honor it - act = fsm.resume(true, st, asio::cancellation_type_t::total); + act = fsm.resume(true, st, cancellation_type::total); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful read @@ -397,7 +400,7 @@ void test_cancel_notwaiting_total() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -415,13 +418,13 @@ void test_subscription_tracking_success() exec_fsm fsm(std::move(input.elm)); // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -430,7 +433,7 @@ void test_subscription_tracking_success() // The request doesn't have a response, so this will // awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 0u)); // All memory should have been freed by now @@ -456,13 +459,13 @@ void test_subscription_tracking_error() exec_fsm fsm(std::move(input.elm)); // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a write error, which would trigger a reconnection @@ -470,8 +473,8 @@ void test_subscription_tracking_error() st.mpx.cancel_on_conn_lost(); // This awakens the request - act = fsm.resume(true, st, cancellation_type_t::none); - BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted, 0u)); + act = fsm.resume(true, st, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled, 0u)); // All memory should have been freed by now BOOST_TEST(input.weak_elm.expired()); diff --git a/test/test_exec_one_fsm.cpp b/test/test_exec_one_fsm.cpp index 1e5d7c024..1069e5916 100644 --- a/test/test_exec_one_fsm.cpp +++ b/test/test_exec_one_fsm.cpp @@ -7,15 +7,15 @@ // #include +#include #include #include #include #include #include -#include -#include #include +#include #include #include "print_node.hpp" @@ -26,13 +26,13 @@ #include using namespace boost::redis; -namespace asio = boost::asio; using detail::exec_one_fsm; using detail::exec_one_action; using detail::exec_one_action_type; using detail::read_buffer; +using detail::cancellation_type; using boost::system::error_code; -using boost::asio::cancellation_type_t; +namespace errc = boost::system::errc; using parse_event = any_adapter::parse_event; using resp3::type; @@ -112,17 +112,17 @@ void test_success() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read the entire response in one go constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -147,11 +147,11 @@ void test_no_expected_response() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM shouldn't ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // No adapter calls should be done @@ -167,25 +167,25 @@ void test_short_reads() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read fragments constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload.substr(0, 6u)); - act = fsm.resume(buff, error_code(), 6u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 6u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(buff, payload.substr(6, 10u)); - act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 10u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(buff, payload.substr(16)); - act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -210,12 +210,12 @@ void test_write_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // Write error - act = fsm.resume(buff, asio::error::connection_reset, 10u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::connection_reset)); + act = fsm.resume(buff, make_error_code(errc::io_error), 10u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_write_cancel() @@ -226,12 +226,12 @@ void test_write_cancel() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // Edge case where the operation finished successfully but with the cancellation state set - act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(buff, error_code(), 10u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Errors in read @@ -243,16 +243,16 @@ void test_read_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read error - act = fsm.resume(buff, asio::error::network_reset, 0u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::network_reset)); + act = fsm.resume(buff, make_error_code(errc::io_error), 0u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_read_cancelled() @@ -263,17 +263,17 @@ void test_read_cancelled() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Edge case where the operation finished successfully but with the cancellation state set copy_to(buff, "$5\r\n"); - act = fsm.resume(buff, error_code(), 4u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(buff, error_code(), 4u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Buffer too small @@ -286,11 +286,11 @@ void test_buffer_prepare_error() buff.set_config({4096u, 8u}); // max size is 8 bytes // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // When preparing the buffer, we encounter an error - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size)); } @@ -303,17 +303,17 @@ void test_parse_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // The response contains an invalid message constexpr std::string_view payload = "$bad\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::not_a_number)); } @@ -329,17 +329,17 @@ void test_adapter_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read the entire response in one go constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); } diff --git a/test/test_reader_fsm.cpp b/test/test_reader_fsm.cpp index b5cc60c04..7b66ea842 100644 --- a/test/test_reader_fsm.cpp +++ b/test/test_reader_fsm.cpp @@ -6,6 +6,7 @@ // #include +#include #include #include #include @@ -14,9 +15,10 @@ #include #include -#include #include #include +#include +#include #include #include "sansio_utils.hpp" @@ -24,14 +26,15 @@ #include #include #include +#include using namespace boost::redis; -namespace net = boost::asio; using boost::system::error_code; -using net::cancellation_type_t; +namespace errc = boost::system::errc; using detail::reader_fsm; using detail::multiplexer; using detail::connection_state; +using detail::cancellation_type; using action = detail::reader_fsm::action; using namespace std::chrono_literals; @@ -108,11 +111,12 @@ struct fixture : detail::log_fixture { void test_push() { + // Timeout condition is arbitrary fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -124,19 +128,19 @@ void test_push() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // Deliver the 2st push - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(12u)); // Deliver the 3rd push - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(13u)); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Check logging @@ -150,10 +154,10 @@ void test_push() void test_read_needs_more() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Split the incoming message in three random parts and deliver @@ -162,22 +166,22 @@ void test_read_needs_more() // Passes the first part to the fsm. copy_to(fix.st.mpx, msg[0]); - act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the second part to the fsm. copy_to(fix.st.mpx, msg[1]); - act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the third and last part to the fsm, next it should ask us // to deliver the message. copy_to(fix.st.mpx, msg[2]); - act = fsm.resume(fix.st, msg[2].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[2].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(msg[0].size() + msg[1].size() + msg[2].size())); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Check logging @@ -197,11 +201,11 @@ void test_read_needs_more() void test_health_checks_disabled() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; fix.st.cfg.health_check_interval = 0s; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Split the message into two so we cover both the regular read and the needs more branch @@ -209,16 +213,16 @@ void test_health_checks_disabled() // Passes the first part to the fsm. copy_to(fix.st.mpx, msg[0]); - act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Push delivery complete copy_to(fix.st.mpx, msg[1]); - act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(25u)); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Check logging @@ -235,10 +239,10 @@ void test_health_checks_disabled() void test_read_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -246,7 +250,7 @@ void test_read_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {error::empty_field}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {error::empty_field}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::empty_field}); // Check logging @@ -263,14 +267,14 @@ void test_read_error() void test_read_timeout() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); - // Timeout - act = fsm.resume(fix.st, 0, {net::error::operation_aborted}, cancellation_type_t::none); + // Timeout: an error code that matches the timeout condition + act = fsm.resume(fix.st, 0, make_error_code(errc::broken_pipe), cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::pong_timeout}); // Check logging @@ -286,10 +290,10 @@ void test_read_timeout() void test_parse_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -297,7 +301,7 @@ void test_parse_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::not_a_number}); // Check logging @@ -317,7 +321,7 @@ void test_setup_request_error() { // Setup fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; request req; req.push("PING"); // should have 1 command auto elem = std::make_shared( @@ -333,7 +337,7 @@ void test_setup_request_error() BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size())); // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -341,7 +345,7 @@ void test_setup_request_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::resp3_hello}); // Check logging @@ -355,10 +359,10 @@ void test_setup_request_error() void test_push_deliver_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -366,11 +370,11 @@ void test_push_deliver_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // Resumes from notifying a push with an error. - act = fsm.resume(fix.st, 0, error::empty_field, cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::empty_field}); // Check logging @@ -389,16 +393,16 @@ void test_max_read_buffer_size() fix.st.cfg.read_buffer_append_size = 5; fix.st.cfg.max_read_size = 7; fix.st.mpx.set_config(fix.st.cfg); - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the first part to the fsm. std::string const part1 = ">3\r\n"; copy_to(fix.st.mpx, part1); - act = fsm.resume(fix.st, part1.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, part1.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size)); // Check logging @@ -416,21 +420,22 @@ void test_max_read_buffer_size() void test_cancel_read() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); - // The read was cancelled (maybe after delivering some bytes) + // The read was cancelled (maybe after delivering some bytes). + // Cancellation state wins to timeout. constexpr std::string_view payload = ">1\r\n"; copy_to(fix.st.mpx, payload); act = fsm.resume( fix.st, payload.size(), - net::error::operation_aborted, - cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + boost::asio::error::operation_aborted, + cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -442,18 +447,18 @@ void test_cancel_read() void test_cancel_read_edge() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Deliver a push, and notify a cancellation. // This can happen if the cancellation signal arrives before the read handler runs constexpr std::string_view payload = ">1\r\n+msg1\r\n"; copy_to(fix.st.mpx, payload); - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -465,10 +470,10 @@ void test_cancel_read_edge() void test_cancel_push_delivery() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -479,12 +484,13 @@ void test_cancel_push_delivery() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); - // We got a cancellation while delivering it - act = fsm.resume(fix.st, 0, net::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + // We got a cancellation while delivering it. + // The pass-through ec is superseded by the cancellation state. + act = fsm.resume(fix.st, 0, boost::asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -497,10 +503,10 @@ void test_cancel_push_delivery() void test_cancel_push_delivery_edge() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -511,13 +517,13 @@ void test_cancel_push_delivery_edge() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // We got a cancellation after delivering it. // This can happen if the cancellation signal arrives before the channel send handler runs - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ diff --git a/test/test_run_fsm.cpp b/test/test_run_fsm.cpp index 25558fd5f..92d3c0326 100644 --- a/test/test_run_fsm.cpp +++ b/test/test_run_fsm.cpp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -14,8 +15,8 @@ #include #include -#include // for BOOST_ASIO_HAS_LOCAL_SOCKETS #include +#include #include #include "sansio_utils.hpp" @@ -24,13 +25,14 @@ #include using namespace boost::redis; +namespace errc = boost::system::errc; namespace asio = boost::asio; using detail::run_fsm; using detail::multiplexer; using detail::run_action_type; using detail::run_action; +using detail::cancellation_type; using boost::system::error_code; -using boost::asio::cancellation_type_t; using namespace std::chrono_literals; // Operators @@ -85,8 +87,9 @@ struct fixture : detail::log_fixture { return res; } - fixture(config&& cfg = default_config()) + fixture(config&& cfg = default_config(), bool unix_sockets_supported = true) : st{{make_logger()}, std::move(cfg)} + , fsm{unix_sockets_supported} { } }; @@ -98,18 +101,17 @@ config config_no_reconnect() } // Config errors -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS void test_config_error_unix() { // Setup config cfg; cfg.unix_socket = "/var/sock"; - fixture fix{std::move(cfg)}; + fixture fix{std::move(cfg), false}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::unix_sockets_unsupported)); // Log @@ -119,7 +121,6 @@ void test_config_error_unix() "are not supported by the system. [boost.redis:24]"}, }); } -#endif void test_config_error_unix_ssl() { @@ -130,9 +131,9 @@ void test_config_error_unix_ssl() fixture fix{std::move(cfg)}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::unix_sockets_ssl_unsupported)); // Log @@ -154,9 +155,9 @@ void test_config_error_unix_sentinel() fixture fix{std::move(cfg)}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_unix_sockets_unsupported)); // Log @@ -174,17 +175,17 @@ void test_connect_error() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -207,17 +208,17 @@ void test_connect_error_ssl() fix.st.cfg.use_ssl = true; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -231,7 +232,6 @@ void test_connect_error_ssl() }); } -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS void test_connect_error_unix() { // Setup @@ -239,17 +239,17 @@ void test_connect_error_unix() fix.st.cfg.unix_socket = "/tmp/sock"; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -262,7 +262,6 @@ void test_connect_error_unix() // clang-format on }); } -#endif // An error in connect without reconnection enabled makes the operation finish void test_connect_error_no_reconnect() @@ -271,11 +270,11 @@ void test_connect_error_no_reconnect() fixture fix{config_no_reconnect()}; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. The operation finishes - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::connect_timeout)); // Log @@ -294,12 +293,15 @@ void test_connect_cancel() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect cancelled. The operation finishes - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume( + fix.st, + make_error_code(errc::operation_canceled), + cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -315,12 +317,12 @@ void test_connect_cancel_edge() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect cancelled. The operation finishes - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -337,19 +339,19 @@ void test_parallel_group_error() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep and connect again - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -368,15 +370,15 @@ void test_parallel_group_error_no_reconnect() fixture fix{config_no_reconnect()}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We cancel the receive operation and exit - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); // Log @@ -394,16 +396,16 @@ void test_parallel_group_cancel() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits because the operation gets cancelled. Any receive operation gets cancelled - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -419,16 +421,16 @@ void test_parallel_group_cancel_no_reconnect() fixture fix{config_no_reconnect()}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits because the operation gets cancelled. Any receive operation gets cancelled - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -445,20 +447,20 @@ void test_wait_cancel() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // We get cancelled during the sleep - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -474,20 +476,20 @@ void test_wait_cancel_edge() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // We get cancelled during the sleep - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -503,32 +505,32 @@ void test_several_reconnections() fixture fix; // Run the operation. Connect errors and we sleep - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // Connect again, this time successfully. We launch the tasks - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep and connect again - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Exit with cancellation - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -555,9 +557,9 @@ void test_setup_ping_requests() fixture fix{std::move(cfg)}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the requests are set up @@ -568,13 +570,13 @@ void test_setup_ping_requests() BOOST_TEST_EQ(fix.st.setup_req.payload(), expected_setup); // Reconnect - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // The requests haven't been modified @@ -591,9 +593,9 @@ void test_setup_request_success() fix.st.cfg.setup.push("HELLO", 3); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. Simulate the writer @@ -625,9 +627,9 @@ void test_setup_request_empty() fix.st.cfg.setup.clear(); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Nothing was added to the multiplexer @@ -652,9 +654,9 @@ void test_setup_request_server_error() fix.st.cfg.setup.push("HELLO", 3); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. Simulate the writer @@ -687,9 +689,9 @@ void test_setup_request_other_error() fix.st.cfg.reconnect_wait_interval = 0s; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. @@ -705,9 +707,9 @@ void test_setup_request_other_error() BOOST_TEST(res.first == detail::consume_result::got_response); // This will cause the writer to exit - act = fix.fsm.resume(fix.st, error::not_a_number, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::not_a_number, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::not_a_number)); // Check log @@ -730,21 +732,21 @@ void test_sentinel_reconnection() }; // Resolve succeeds, and a connection is attempted - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host1", "1000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This errors, so we sleep and resolve again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host2", "2000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Sentinel involves always a setup request containing the role check. Run it. @@ -757,19 +759,19 @@ void test_sentinel_reconnection() BOOST_TEST(res.first == detail::consume_result::got_response); // The parallel group errors, so we sleep and resolve again - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host3", "3000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Cancel - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -795,18 +797,18 @@ void test_sentinel_resolve_error() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); // It fails with an error, so we go to sleep - act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // Retrying it succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"myhost", "10000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Log @@ -825,11 +827,11 @@ void test_sentinel_resolve_error_no_reconnect() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); // It fails with an error, so we exit - act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // Log @@ -845,10 +847,10 @@ void test_sentinel_resolve_cancel() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -860,17 +862,13 @@ void test_sentinel_resolve_cancel() int main() { -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS test_config_error_unix(); -#endif test_config_error_unix_ssl(); test_config_error_unix_sentinel(); test_connect_error(); test_connect_error_ssl(); -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS test_connect_error_unix(); -#endif test_connect_error_no_reconnect(); test_connect_cancel(); test_connect_cancel_edge(); diff --git a/test/test_sentinel_resolve_fsm.cpp b/test/test_sentinel_resolve_fsm.cpp index d3046819a..fe6e94009 100644 --- a/test/test_sentinel_resolve_fsm.cpp +++ b/test/test_sentinel_resolve_fsm.cpp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -23,6 +25,8 @@ using namespace boost::redis; namespace asio = boost::asio; +namespace errc = boost::system::errc; +using detail::cancellation_type; using detail::sentinel_resolve_fsm; using detail::sentinel_action; using detail::connection_state; @@ -109,11 +113,11 @@ void test_success() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -125,7 +129,7 @@ void test_success() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -160,11 +164,11 @@ void test_success_replica() fix.st.eng.get().seed(static_cast(183984887232u)); // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -181,7 +185,7 @@ void test_success_replica() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The address of one of the replicas is stored @@ -205,15 +209,15 @@ void test_one_connect_error() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // This errors, so we connect to the 2nd sentinel - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", @@ -221,7 +225,7 @@ void test_one_connect_error() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -247,21 +251,21 @@ void test_one_request_network_error() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); // It fails, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -288,9 +292,9 @@ void test_one_request_parse_error() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "+OK\r\n", @@ -298,15 +302,15 @@ void test_one_request_parse_error() }); // This fails parsing, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -334,9 +338,9 @@ void test_one_request_error_node() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR needs authentication\r\n", @@ -344,15 +348,15 @@ void test_one_request_error_node() }); // This fails, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -379,9 +383,9 @@ void test_one_master_unknown() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", @@ -390,15 +394,15 @@ void test_one_master_unknown() // It doesn't know about our master, so we connect to the 2nd sentinel. // This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -426,9 +430,9 @@ void test_one_no_replicas() fix.st.cfg.sentinel.server_role = role::replica; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", @@ -437,9 +441,9 @@ void test_one_no_replicas() }); // This errors, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -450,7 +454,7 @@ void test_one_no_replicas() "*0\r\n", // clang-format on }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The replica's address is stored @@ -477,9 +481,9 @@ void test_error() fixture fix; // 1st Sentinel doesn't know about the master - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", @@ -487,13 +491,13 @@ void test_error() }); // Move to the 2nd Sentinel, which fails to connect - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); // Move to the 3rd Sentinel, which has authentication misconfigured - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host3", "3000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR unauthorized\r\n", @@ -501,7 +505,7 @@ void test_error() }); // Sentinel list exhausted - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // The Sentinel list is not updated @@ -542,16 +546,16 @@ void test_error_replica() fix.st.cfg.sentinel.server_role = role::replica; // Initiate, connect to the only Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // Logs @@ -576,12 +580,12 @@ void test_cancel_connect() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Cancellation - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -597,12 +601,12 @@ void test_cancel_connect_edge() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Cancellation (without error code) - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -618,12 +622,12 @@ void test_cancel_request() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -640,12 +644,12 @@ void test_cancel_request_edge() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ diff --git a/test/test_unix_sockets.cpp b/test/test_unix_sockets.cpp index ec53b5836..eefaa1f4b 100644 --- a/test/test_unix_sockets.cpp +++ b/test/test_unix_sockets.cpp @@ -48,7 +48,7 @@ void test_exec() // Run the connection conn.async_run(cfg, {}, [&run_finished](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Execute a request @@ -93,7 +93,7 @@ void test_reconnection() // Run the connection conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // The PING is the end of the callback chain @@ -138,13 +138,13 @@ void test_switch_between_transports() auto on_run_tls_2 = [&](error_code ec) { finished = true; std::cout << "Run (TCP/TLS 2) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; // After UNIX sockets, switch back to TCP/tLS auto on_run_unix = [&](error_code ec) { std::cout << "Run (UNIX) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); // Change to using TCP with TLS again conn.async_run(unix_cfg, {}, on_run_tls_2); @@ -159,7 +159,7 @@ void test_switch_between_transports() // After TCP/TLS, change to UNIX sockets auto on_run_tls_1 = [&](error_code ec) { std::cout << "Run (TCP/TLS 1) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); conn.async_run(unix_cfg, {}, on_run_unix); conn.async_exec(req, res2, [&](error_code ec, std::size_t) { diff --git a/test/test_writer_fsm.cpp b/test/test_writer_fsm.cpp index 0c151a3f7..cc7e3e0a8 100644 --- a/test/test_writer_fsm.cpp +++ b/test/test_writer_fsm.cpp @@ -6,16 +6,19 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include +#include #include #include -#include #include #include +#include #include +#include #include #include "sansio_utils.hpp" @@ -24,8 +27,11 @@ #include #include #include +#include using namespace boost::redis; +using boost::system::error_code; +namespace errc = boost::system::errc; namespace asio = boost::asio; using detail::writer_fsm; using detail::multiplexer; @@ -33,8 +39,7 @@ using detail::writer_action_type; using detail::consume_result; using detail::writer_action; using detail::connection_state; -using boost::system::error_code; -using boost::asio::cancellation_type_t; +using detail::cancellation_type; using namespace std::chrono_literals; // Operators @@ -109,7 +114,8 @@ struct test_elem { struct fixture : detail::log_fixture { connection_state st{{make_logger()}}; - writer_fsm fsm; + // Timeout condition is arbitrary + writer_fsm fsm{std::errc::broken_pipe}; fixture() { @@ -129,13 +135,12 @@ void test_single_request() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // The write completes successfully. The request is written, and we go back to sleep. - act = fix.fsm - .resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item1.elm->is_written()); @@ -143,13 +148,12 @@ void test_single_request() fix.st.mpx.add(item2.elm); // The wait is cancelled to signal we've got a new request - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item2.elm->is_staged()); // Write successful - act = fix.fsm - .resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item2.elm->is_written()); @@ -171,7 +175,7 @@ void test_request_arrives_while_writing() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); @@ -180,15 +184,13 @@ void test_request_arrives_while_writing() // The write completes successfully. The request is written, // and we start writing the new one - act = fix.fsm - .resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_written()); BOOST_TEST(item2.elm->is_staged()); // Write successful - act = fix.fsm - .resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item2.elm->is_written()); @@ -207,19 +209,50 @@ void test_no_request_at_startup() test_elem item; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // A request arrives fix.st.mpx.add(item.elm); - // The wait is cancelled to signal we've got a new request - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + // The wait is cancelled (with a non-empty ec signal) to indicate new data + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::write_some(4s)); + BOOST_TEST(item.elm->is_staged()); + + // Write successful + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::wait(4s)); + BOOST_TEST(item.elm->is_written()); + + // Logs + fix.check_log({ + {logger::level::debug, "Writer task: 24 bytes written."}, + }); +} + +// If the wait is signaled with any error, we considered it a notification. +// Important because Asio and Corosio might signal this differently +void test_wait_canceled_other_code() +{ + // Setup + fixture fix; + test_elem item; + + // Start. There is no request, so we wait + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::wait(4s)); + + // A request arrives + fix.st.mpx.add(item.elm); + + // The wait is cancelled (with a non-empty ec signal) to indicate new data + act = fix.fsm.resume(fix.st, boost::capy::error::canceled, 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write successful - act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item.elm->is_written()); @@ -240,27 +273,27 @@ void test_short_writes() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // We write a few bytes. It's not the entire message, so we write again - act = fix.fsm.resume(fix.st, error_code(), 2u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 2u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // We write some more bytes, but still not the entire message. - act = fix.fsm.resume(fix.st, error_code(), 5u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 5u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // A zero size write doesn't cause trouble - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // Complete writing the message (the entire payload is 24 bytes long) - act = fix.fsm.resume(fix.st, error_code(), 17u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 17u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item1.elm->is_written()); @@ -282,16 +315,16 @@ void test_ping() constexpr std::string_view ping_payload = "*2\r\n$4\r\nPING\r\n$8\r\nping_msg\r\n"; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // No request arrives during the wait interval so a ping is added - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST_EQ(fix.st.mpx.get_write_buffer(), ping_payload); // Write successful - act = fix.fsm.resume(fix.st, error_code(), ping_payload.size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), ping_payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Simulate a successful response to the PING @@ -320,12 +353,12 @@ void test_health_checks_disabled() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(0s)); BOOST_TEST(item.elm->is_staged()); // The write completes successfully. The request is written, and we go back to sleep. - act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(0s)); BOOST_TEST(item.elm->is_written()); @@ -343,16 +376,16 @@ void test_ping_error() error_code ec; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // No request arrives during the wait interval so a ping is added - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); // Write successful const auto ping_size = fix.st.mpx.get_write_buffer().size(); - act = fix.fsm.resume(fix.st, error_code(), ping_size, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), ping_size, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Simulate an error response to the PING @@ -381,14 +414,14 @@ void test_write_error() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // The write completes with an error (possibly with partial success). // The request is still staged, and the writer exits. // Use an error we control so we can check logs - act = fix.fsm.resume(fix.st, error::empty_field, 2u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, 2u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); BOOST_TEST(item.elm->is_staged()); @@ -410,12 +443,12 @@ void test_write_timeout() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); - // The write times out, so it completes with operation_aborted - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + // The write times out. An ec matching the timeout condition is reported. + act = fix.fsm.resume(fix.st, make_error_code(errc::broken_pipe), 0u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::write_timeout)); BOOST_TEST(item.elm->is_staged()); @@ -439,13 +472,13 @@ void test_cancel_write() fix.st.mpx.add(item.elm); // Start. A write is triggered - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write cancelled and failed with operation_aborted - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 2u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 2u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_staged()); // Logs @@ -466,14 +499,14 @@ void test_cancel_write_edge() fix.st.mpx.add(item.elm); // Start. A write is triggered - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write cancelled but without error act = fix.fsm - .resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + .resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_written()); // Logs @@ -491,19 +524,16 @@ void test_cancel_wait() test_elem item; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Sanity check: the writer doesn't touch the multiplexer after a cancellation fix.st.mpx.add(item.elm); - // Cancel the wait, setting the cancellation state - act = fix.fsm.resume( - fix.st, - asio::error::operation_aborted, - 0u, - asio::cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + // Cancel the wait, setting the cancellation state. + // The pass-through ec is superseded by the cancellation state. + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_waiting()); // Logs @@ -519,6 +549,7 @@ int main() test_single_request(); test_request_arrives_while_writing(); test_no_request_at_startup(); + test_wait_canceled_other_code(); test_short_writes(); test_health_checks_disabled();