diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ae265711..6ee212245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,8 @@ jobs: - name: Build a Boost distribution using B2 run: | python3 tools/ci.py build-b2-distro \ - --toolset ${{ matrix.toolset }} + --toolset ${{ matrix.toolset }} \ + --cxxstd ${{ matrix.cxxstd }} # No Redis server is available on this job, so integration tests are skipped. # Unit tests are built and run via the CMake superproject build. @@ -48,8 +49,7 @@ jobs: --toolset ${{ matrix.toolset }} \ --generator "${{ matrix.generator }}" \ --build-shared-libs ${{ matrix.build-shared-libs }} \ - --integration-tests 0 \ - "--cxxflags=//WX" + --integration-tests 0 - name: Run add_subdirectory tests run: | @@ -58,8 +58,7 @@ jobs: --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ --generator "${{ matrix.generator }}" \ - --build-shared-libs ${{ matrix.build-shared-libs }} \ - "--cxxflags=//WX" + --build-shared-libs ${{ matrix.build-shared-libs }} - name: Run find_package tests with the built cmake distribution run: | @@ -68,8 +67,7 @@ jobs: --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ --generator "${{ matrix.generator }}" \ - --build-shared-libs ${{ matrix.build-shared-libs }} \ - "--cxxflags=//WX" + --build-shared-libs ${{ matrix.build-shared-libs }} - name: Run find_package tests with the built b2 distribution run: | @@ -78,8 +76,7 @@ jobs: --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ --generator "${{ matrix.generator }}" \ - --build-shared-libs ${{ matrix.build-shared-libs }} \ - "--cxxflags=//WX" + --build-shared-libs ${{ matrix.build-shared-libs }} windows-b2: name: "B2 ${{matrix.toolset}}" @@ -127,7 +124,7 @@ jobs: cxxstd: '17' build-type: 'Debug' server: "redis:8.2.1-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: gcc-11 install: g++-11 @@ -135,7 +132,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "valkey/valkey:8.1.3-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: gcc-11 install: g++-11 @@ -143,7 +140,7 @@ jobs: cxxstd: '20' build-type: 'Release' server: "redis:7.4.5-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: gcc-12 install: g++-12 @@ -159,7 +156,7 @@ jobs: cxxstd: '17' build-type: 'Debug' server: "valkey/valkey:8.1.3-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: gcc-13 install: g++-13 @@ -189,7 +186,7 @@ jobs: cxxstd: '17' build-type: 'Debug' server: "redis:7.4.5-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: gcc-15 install: g++-15 @@ -213,7 +210,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "redis:7.4.5-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-12 install: clang-12 @@ -221,7 +218,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "redis:8.2.1-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-13 install: clang-13 @@ -229,7 +226,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "valkey/valkey:8.1.3-alpine" - corosio-tests: '0' + corosio-api: '0' # clang-14 is the default in Ubuntu 22.04 - toolset: clang-14 @@ -238,7 +235,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "redis:7.4.5-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-14 install: 'clang-14 libc++-14-dev libc++abi-14-dev' @@ -248,7 +245,7 @@ jobs: cxxflags: '-stdlib=libc++' ldflags: '-lc++' server: "redis:8.2.1-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-14 install: 'clang-14 libc++-14-dev libc++abi-14-dev' @@ -258,6 +255,7 @@ jobs: cxxflags: '-stdlib=libc++' ldflags: '-lc++' server: "valkey/valkey:8.1.3-alpine" + corosio-api: '0' - toolset: clang-15 install: clang-15 @@ -265,7 +263,7 @@ jobs: cxxstd: '20' build-type: 'Debug' server: "redis:7.4.5-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-16 install: clang-16 @@ -274,7 +272,7 @@ jobs: build-type: 'Debug' cxxflags: '-DBOOST_ASIO_DISABLE_LOCAL_SOCKETS=1' # If a system has no UNIX socket support, we build correctly server: "redis:8.2.1-alpine" - corosio-tests: '0' + corosio-api: '0' - toolset: clang-17 install: clang-17 @@ -299,6 +297,7 @@ jobs: cxxflags: '-stdlib=libc++' ldflags: '-lc++' server: "valkey/valkey:8.1.3-alpine" + corosio-api: '0' # This libc++ version doesn't have stop_token - toolset: clang-18 install: 'clang-18 libc++-18-dev libc++abi-18-dev' @@ -308,6 +307,7 @@ jobs: cxxflags: '-stdlib=libc++' ldflags: '-lc++' server: "redis:8.2.1-alpine" + corosio-api: '0' # This libc++ version doesn't have stop_token - toolset: clang-19 install: clang-19 @@ -392,7 +392,8 @@ jobs: - name: Build a Boost distribution using B2 run: | docker exec builder /boost-redis/tools/ci.py build-b2-distro \ - --toolset ${{ matrix.toolset }} + --toolset ${{ matrix.toolset }} \ + --cxxstd ${{ matrix.cxxstd }} # The CMake superproject build also drives the project tests, including # integration tests against the Redis server set up above. @@ -402,6 +403,7 @@ jobs: --build-type ${{ matrix.build-type }} \ --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ + --corosio-api ${{ matrix.corosio-api == '' && '1' || matrix.corosio-api }} \ "--cxxflags=${{ matrix.cxxflags }} -Werror" \ "--ldflags=${{ matrix.ldflags }}" @@ -411,6 +413,7 @@ jobs: --build-type ${{ matrix.build-type }} \ --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ + --corosio-api ${{ matrix.corosio-api == '' && '1' || matrix.corosio-api }} \ "--cxxflags=${{ matrix.cxxflags }} -Werror" \ "--ldflags=${{ matrix.ldflags }}" @@ -420,6 +423,7 @@ jobs: --build-type ${{ matrix.build-type }} \ --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ + --corosio-api ${{ matrix.corosio-api == '' && '1' || matrix.corosio-api }} \ "--cxxflags=${{ matrix.cxxflags }} -Werror" \ "--ldflags=${{ matrix.ldflags }}" @@ -429,6 +433,7 @@ jobs: --build-type ${{ matrix.build-type }} \ --cxxstd ${{ matrix.cxxstd }} \ --toolset ${{ matrix.toolset }} \ + --corosio-api ${{ matrix.corosio-api == '' && '1' || matrix.corosio-api }} \ "--cxxflags=${{ matrix.cxxflags }} -Werror" \ "--ldflags=${{ matrix.ldflags }}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6db4a672f..8ecdd03f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,12 +2,6 @@ cmake_minimum_required(VERSION 3.8...4.2) project(boost_redis VERSION "${BOOST_SUPERPROJECT_VERSION}" LANGUAGES CXX) -# Library -add_library(boost_redis INTERFACE) -add_library(Boost::redis ALIAS boost_redis) -target_include_directories(boost_redis INTERFACE include) -target_compile_features(boost_redis INTERFACE cxx_std_17) - # Dependencies # Boost dependencies should be already available. # If other dependencies are not found, we bail out @@ -22,24 +16,56 @@ if(NOT OpenSSL_FOUND) return() endif() -# This is generated by boostdep -target_link_libraries(boost_redis +# Allow explicit control over the Corosio API. +# TODO: this is temporary, until Capy gets into Boost. +# https://github.com/cppalliance/capy/issues/274 +option(BOOST_REDIS_COROSIO_API OFF "Whether to build the Corosio API") + +# Library (common) +add_library(boost_redis_proto INTERFACE) +add_library(Boost::redis_proto ALIAS boost_redis_proto) +target_include_directories(boost_redis_proto INTERFACE include) +target_compile_features(boost_redis_proto INTERFACE cxx_std_17) +target_link_libraries(boost_redis_proto INTERFACE - Boost::asio Boost::assert Boost::core Boost::mp11 Boost::system Boost::throw_exception +) + +# Library (Asio) +add_library(boost_redis INTERFACE) +add_library(Boost::redis ALIAS boost_redis) +target_link_libraries(boost_redis + INTERFACE + Boost::redis_proto + Boost::asio Threads::Threads OpenSSL::Crypto OpenSSL::SSL ) +# Library (Corosio) +if (BOOST_REDIS_COROSIO_API) + add_library(boost_redis_corosio INTERFACE) + add_library(Boost::redis_corosio ALIAS boost_redis_corosio) + target_compile_features(boost_redis_corosio INTERFACE cxx_std_20) + target_link_libraries(boost_redis_corosio + INTERFACE + Boost::redis_proto + Boost::capy + Boost::corosio + Boost::corosio_openssl + ) +endif() + # Don't run integration testing unless explicitly requested, since these require a running server option(BOOST_REDIS_INTEGRATION_TESTS OFF "Whether to build and run integration tests or not") mark_as_advanced(BOOST_REDIS_INTEGRATION_TESTS) + # Examples and tests if(BUILD_TESTING) # Custom target tests; required by the Boost superproject @@ -59,3 +85,15 @@ if(BUILD_TESTING) add_subdirectory(example) endif() endif() + +if(BOOST_SUPERPROJECT_VERSION AND NOT CMAKE_VERSION VERSION_LESS 3.13) + set(_boost_redis_install_targets boost_redis boost_redis_proto) + if (BOOST_REDIS_COROSIO_API) + list(APPEND _boost_redis_install_targets boost_redis_corosio) + endif() + boost_install( + TARGETS ${_boost_redis_install_targets} + VERSION ${BOOST_SUPERPROJECT_VERSION} + HEADER_DIRECTORY include + ) +endif() diff --git a/README.md b/README.md index fae5cc684..3026b4bf9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Boost.Redis -Boost.Redis is a high-level [Redis](https://redis.io/) client library built on top of +Boost.Redis is a high-level [Redis](https://redis.io/) client library for [Boost.Asio](https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html) +and [Corosio](https://github.com/cppalliance/corosio/) that implements the Redis protocol [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md). @@ -12,18 +13,24 @@ Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/in The requirements for using Boost.Redis are: * Boost 1.84 or higher. Boost.Redis is included in Boost installations since Boost 1.84. -* C++17 or higher. Supported compilers include gcc 11 and later, clang 11 and later, and Visual Studio 16 (2019) and later. +* When using Asio, C++17 or higher. Supported compilers include gcc 11 and later, clang 11 and later, and MSVC 14.20 and later. +* When using Corosio, C++20 or higher. Supported compilers include gcc 12 and later, clang 17 and later, MSVC 14.34 and later. * Redis 6 or higher (must support RESP3). * OpenSSL. -The documentation assumes basic-level knowledge about [Redis](https://redis.io/docs/) and [Boost.Asio](https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html). +The documentation assumes basic-level knowledge about [Redis](https://redis.io/docs/) and either Boost.Asio or Corosio. ## Building the library To use the library it is necessary to include the following: ```cpp +// If using the Asio API #include + +// If using the Corosio API +#include +#include ``` in exactly one source file in your applications. Otherwise, the library is header-only. @@ -77,6 +84,55 @@ The roles played by the `async_run` and `async_exec` functions are: be called from multiple places in your code concurrently. * `connection::async_run`: keeps the connection healthy. It takes care of hostname resolution, session establishment, health-checks, reconnection and coordination of low-level read and write operations. It should be called only once per connection, regardless of the number of requests to execute. +The Corosio API works similarly: + +```cpp +#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{})); +} +``` + ## Server pushes Redis servers can also send a variety of pushes to the client. Some of diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index d3f35cbcb..348407d78 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -1,11 +1,13 @@ add_library(boost_redis_benchmarks_options INTERFACE) -target_link_libraries(boost_redis_benchmarks_options INTERFACE boost_redis_src) +target_link_libraries(boost_redis_benchmarks_options INTERFACE boost_redis_asio_src) target_link_libraries(boost_redis_benchmarks_options INTERFACE boost_redis_project_options) target_compile_features(boost_redis_benchmarks_options INTERFACE cxx_std_20) add_executable(boost_redis_echo_server_client cpp/asio/echo_server_client.cpp) target_link_libraries(boost_redis_echo_server_client PRIVATE boost_redis_benchmarks_options) +add_dependencies(tests boost_redis_echo_server_client) add_executable(boost_redis_echo_server_direct cpp/asio/echo_server_direct.cpp) target_link_libraries(boost_redis_echo_server_direct PRIVATE boost_redis_benchmarks_options) +add_dependencies(tests boost_redis_echo_server_direct) diff --git a/build.jam b/build.jam index 5c5c19b86..3ebb043d3 100644 --- a/build.jam +++ b/build.jam @@ -5,13 +5,26 @@ require-b2 5.2 ; -constant boost_dependencies : - /boost/asio//boost_asio +constant boost_dependencies_proto : /boost/assert//boost_assert /boost/core//boost_core /boost/mp11//boost_mp11 /boost/system//boost_system - /boost/throw_exception//boost_throw_exception ; + /boost/throw_exception//boost_throw_exception +; + +constant boost_dependencies_asio : + /boost/redis//boost_redis_proto + /boost/asio//boost_asio +; + +# Workaround for https://github.com/bfgroup/b2/issues/596 +constant boost_dependencies_corosio : + /boost/redis//boost_redis_proto + /boost/capy//boost_capy/shared + /boost/corosio//boost_corosio/shared + /boost/corosio//boost_corosio_openssl/shared +; project /boost/redis : common-requirements @@ -19,8 +32,10 @@ project /boost/redis ; explicit - [ alias boost_redis : : : : $(boost_dependencies) ] - [ alias all : boost_redis test ] + [ alias boost_redis_proto : : : : $(boost_dependencies_proto) ] + [ alias boost_redis : : : : $(boost_dependencies_asio) ] + [ alias boost_redis_corosio : : : : $(boost_dependencies_corosio) ] + [ alias all : boost_redis_proto boost_redis boost_redis_corosio test ] ; call-if : boost-library redis diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 00388cb70..82d23bff4 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -17,10 +17,13 @@ project(boost_redis_mrdocs LANGUAGES CXX) # only to our target of interest set(CMAKE_EXPORT_COMPILE_COMMANDS OFF) +# We want docs for the Corosio API, too +set(BOOST_REDIS_COROSIO_API ON) + # Add Boost add_subdirectory($ENV{BOOST_SRC_DIR} deps/boost) # Add the target for mrdocs to analyze add_executable(mrdocs mrdocs.cpp) -target_link_libraries(mrdocs PRIVATE Boost::redis) +target_link_libraries(mrdocs PRIVATE Boost::redis Boost::redis_corosio) set_target_properties(mrdocs PROPERTIES EXPORT_COMPILE_COMMANDS ON) diff --git a/doc/modules/ROOT/pages/auth.adoc b/doc/modules/ROOT/pages/auth.adoc index 59e1644fc..9dc38e3c4 100644 --- a/doc/modules/ROOT/pages/auth.adoc +++ b/doc/modules/ROOT/pages/auth.adoc @@ -23,6 +23,11 @@ and build the desired setup request in without auth). To authenticate, clear the default setup and push a `HELLO` command that includes your credentials: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- config cfg; @@ -30,20 +35,56 @@ cfg.use_setup = true; cfg.setup.clear(); // Remove the default HELLO 3 cfg.setup.hello("my_username", "my_password"); -co_await conn.async_run(cfg); +co_await conn->async_run(cfg); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +config cfg; +cfg.use_setup = true; +cfg.setup.clear(); // Remove the default HELLO 3 +cfg.setup.hello("my_username", "my_password"); + +co_await conn.run(cfg); +---- +-- +======== Authentication is just one use of this mechanism. For example, to select a particular logical database (see the Redis https://redis.io/commands/select/[SELECT] command), add a `SELECT` command after the `HELLO`: +[tabs] +======== +Asio:: ++ +-- +[source,cpp] +---- +config cfg; +cfg.use_setup = true; +cfg.setup.push("SELECT", 42); // select the logical database 42 after the default HELLO 3 + +co_await conn->async_run(cfg); +---- +-- + +Corosio:: ++ +-- [source,cpp] ---- config cfg; cfg.use_setup = true; cfg.setup.push("SELECT", 42); // select the logical database 42 after the default HELLO 3 -co_await conn.async_run(cfg); +co_await conn.run(cfg); ---- +-- +======== diff --git a/doc/modules/ROOT/pages/cancellation.adoc b/doc/modules/ROOT/pages/cancellation.adoc index 78ed20076..b1bad1611 100644 --- a/doc/modules/ROOT/pages/cancellation.adoc +++ b/doc/modules/ROOT/pages/cancellation.adoc @@ -8,16 +8,23 @@ = Cancellation and timeouts -By default, running a request with xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`] -will wait until a connection to the Redis server is established by `async_run`. +By default, executing a request will wait until a connection to the Redis server is established by +`connection::async_run` / `co_connection::run`. This may take a very long time if the server is down. -For this reason, it is usually a good idea to set a timeout to `async_exec` -operations using the +For this reason, it is usually a good idea to set a timeout to request execution. +The specifics depend on whether you are using Asio or Corosio, but the +general idea is similar: + +[tabs] +======== +Asio:: ++ +-- +Use the https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_after.html[`asio::cancel_after`] completion token: - [source,cpp] ---- using namespace std::chrono_literals; @@ -28,32 +35,57 @@ req.push("SET", "my_key", 42); // If the request hasn't completed after 10 seconds, it will be cancelled // and an exception will be thrown. -co_await conn.async_exec(req, ignore, asio::cancel_after(10s)); +co_await conn->async_exec(req, ignore, asio::cancel_after(10s)); ---- See our {site-url}/example/cpp20_timeouts.cpp[example on timeouts] for a full code listing. -You can also use `cancel_after` with other completion styles, like -callbacks and futures. - -`cancel_after` works because `async_exec` supports the per-operation -cancellation mechanism. This is used by Boost.Asio to implement features +`cancel_after` also works with other completion styles, like +callbacks and futures. It works because `async_exec` supports the per-operation +cancellation mechanism, which Boost.Asio uses to implement features like `cancel_after` and parallel groups. All asynchronous operations in the library support this mechanism. Please consult the documentation for individual operations for more info. +-- + +Corosio:: ++ +-- +Wrap the awaitable with `capy::timeout`: + +[source,cpp] +---- +using namespace std::chrono_literals; + +// Compose a request with a SET command +request req; +req.push("SET", "my_key", 42); + +// If the request hasn't completed after 10 seconds, it will be cancelled +// and exec() will complete with an error matching capy::cond::timeout. +auto [ec] = co_await capy::timeout(conn.exec(req, ignore), 10s); +---- + +See our {site-url}/example/corosio_timeouts.cpp[example on timeouts] +for a full code listing. + +`capy::timeout` can be used with any I/O operation exposed by the library. +Combinators like `capy::when_any` and `capy::when_all` also work. +-- +======== == Retrying idempotent requests -We mentioned that `async_exec` waits until the server is up +We mentioned that executing a request waits until the server is up before sending the request. But what happens if there is a communication 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` -error code. +and execution will fail with an error matching +`std::errc::operation_canceled`. Some requests can be executed several times and result in the same outcome as executing them only once. We say that these requests are _idempotent_. @@ -66,6 +98,27 @@ You can do so by setting `cancel_if_unresponded` in xref:reference:boost/redis/request/config.adoc[`request::config`] to false: +[tabs] +======== +Asio:: ++ +-- +[source,cpp] +---- +// Compose a request +request req; +req.push("SET", "my_key", 42); // idempotent +req.get_config().cancel_if_unresponded = false; // Retry the request even if it was written but not responded + +// Makes sure that the key is set, even in the presence of network errors. +// This may suspend for an unspecified period of time if the server is down. +co_await conn->async_exec(req, ignore); +---- +-- + +Corosio:: ++ +-- [source,cpp] ---- // Compose a request @@ -75,5 +128,7 @@ req.get_config().cancel_if_unresponded = false; // Retry the request even if it // Makes sure that the key is set, even in the presence of network errors. // This may suspend for an unspecified period of time if the server is down. -co_await conn.async_exec(req, ignore); +co_await conn.exec(req, ignore); ---- +-- +======== diff --git a/doc/modules/ROOT/pages/examples.adoc b/doc/modules/ROOT/pages/examples.adoc index 454e9b378..fcf3f1c97 100644 --- a/doc/modules/ROOT/pages/examples.adoc +++ b/doc/modules/ROOT/pages/examples.adoc @@ -9,19 +9,21 @@ The examples below show how to use the features discussed throughout this documentation: -* {site-url}/example/cpp20_intro.cpp[cpp20_intro.cpp]: Does not use awaitable operators. -* {site-url}/example/cpp20_intro_tls.cpp[cpp20_intro_tls.cpp]: Communicates over TLS. -* {site-url}/example/cpp20_unix_sockets.cpp[cpp20_unix_sockets.cpp]: Communicates over UNIX domain sockets. -* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions. -* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json. -* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf. -* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment. -* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription. -* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server. -* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub. -* {site-url}/example/cpp17_intro.cpp[cpp17_intro.cpp]: Uses callbacks and requires pass:[C++17]. -* {site-url}/example/cpp17_intro_sync.cpp[cpp17_intro_sync.cpp]: Runs `async_run` in a separate thread and performs synchronous calls to `async_exec`. -* {site-url}/example/cpp17_spdlog.cpp[cpp17_spdlog.cpp]: Shows how to use third-party logging libraries like `spdlog` with Boost.Redis. +* {site-url}/example/cpp20_intro.cpp[cpp20_intro.cpp] / {site-url}/example/corosio_intro.cpp[corosio_intro.cpp]: executes a request. +* {site-url}/example/cpp20_intro_tls.cpp[cpp20_intro_tls.cpp] / {site-url}/example/corosio_intro_tls.cpp[corosio_intro_tls.cpp]: Communicates over TLS. +* {site-url}/example/cpp20_unix_sockets.cpp[cpp20_unix_sockets.cpp] / {site-url}/example/corosio_unix_sockets.cpp[corosio_unix_sockets.cpp]: Communicates over UNIX domain sockets. +* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp] / {site-url}/example/corosio_containers.cpp[corosio_containers.cpp]: Shows how to send and receive STL containers and how to use transactions. +* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp] / {site-url}/example/corosio_json.cpp[corosio_json.cpp]: Shows how to serialize types using Boost.Json. +* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf (Asio only). +* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp] / {site-url}/example/corosio_sentinel.cpp[corosio_sentinel.cpp]: Shows how to use the library with a Sentinel deployment. +* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp] / {site-url}/example/corosio_subscriber.cpp[corosio_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription. +* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp] / {site-url}/example/corosio_echo_server.cpp[corosio_echo_server.cpp]: A simple TCP echo server. +* {site-url}/example/cpp20_timeouts.cpp[cpp20_timeouts.cpp] / {site-url}/example/corosio_timeouts.cpp[corosio_timeouts.cpp]: Demonstrates request-execution timeouts. +* {site-url}/example/cpp20_streams.cpp[cpp20_streams.cpp] / {site-url}/example/corosio_streams.cpp[corosio_streams.cpp]: Demonstrates Redis streams. +* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub (Asio only). +* {site-url}/example/cpp17_intro.cpp[cpp17_intro.cpp]: Uses callbacks and requires pass:[C++17] (Asio only). +* {site-url}/example/cpp17_intro_sync.cpp[cpp17_intro_sync.cpp]: Runs `async_run` in a separate thread and performs synchronous calls to `async_exec` (Asio only). +* {site-url}/example/cpp17_spdlog.cpp[cpp17_spdlog.cpp] / {site-url}/example/corosio_spdlog.cpp[corosio_spdlog.cpp]: Shows how to use third-party logging libraries like `spdlog` with Boost.Redis. The main function used in some async examples has been factored out in the {site-url}/example/main.cpp[main.cpp] file. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 875ca9f97..45831059e 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -9,9 +9,10 @@ = Introduction Boost.Redis is a high-level https://redis.io/[Redis] and https://valkey.io/[Valkey] -client library built on top of +client library offering APIs for https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio] -that implements the Redis protocol +and https://github.com/cppalliance/corosio/[Corosio]. +It implements the Redis protocol https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md[RESP3]. == Requirements @@ -19,21 +20,39 @@ https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md[RESP The requirements for using Boost.Redis are: * Boost 1.84 or higher. Boost.Redis is included in Boost installations since Boost 1.84. -* pass:[C++17] or higher. Supported compilers include gcc 11 and later, clang 11 and later, and Visual Studio 16 (2019) and later. +* For the Boost.Asio API, pass:[C++17] or higher. Supported compilers include gcc 11 and later, clang 11 and later, and MSVC 14.20 and later. +* For the Corosio API, pass:[C++20] or higher. Supported compilers include gcc 12 and later, clang 17 and later, MSVC 14.34 and later. * Redis 6 or higher, or Valkey (any version). The database server must support RESP3. We intend to maintain compatibility with both Redis and Valkey in the long-run. * OpenSSL. -The documentation assumes basic-level knowledge about https://redis.io/docs/[Redis] and https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio]. +The documentation assumes basic-level knowledge about https://redis.io/docs/[Redis] and either Boost.Asio or Corosio. == Building the library To use the library it is necessary to include the following: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- #include ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +#include +#include +---- +-- +======== in exactly one source file in your applications. Otherwise, the library is header-only. @@ -46,6 +65,11 @@ The code below uses a short-lived connection to https://redis.io/commands/ping/[ping] a Redis server: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- #include @@ -79,13 +103,73 @@ auto co_main(config const& cfg) -> net::awaitable std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +#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{})); +} +---- +-- +======== -The roles played by the `async_run` and `async_exec` functions are: +The roles played by the `run` and `exec` functions are: -* xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`]: executes the commands contained in the +* xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`] / + xref:reference:boost/redis/co_connection/exec-00.adoc[`co_connection::exec`]: + executes the commands contained in the request and stores the individual responses in the response object. Can be called from multiple places in your code concurrently. -* xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`]: keeps the connection healthy. It takes care of hostname resolution, session establishment, health-checks, reconnection and coordination of low-level read and write operations. It should be called only once per connection, regardless of the number of requests to execute. +* xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`] / + xref:reference:boost/redis/co_connection/run.adoc[`co_connection::run`]: + keeps the connection healthy. It takes care of hostname resolution, + session establishment, health-checks, reconnection and coordination of low-level read and write + operations. It should be called only once per connection, regardless of the number of requests to execute. == Further reading diff --git a/doc/modules/ROOT/pages/logging.adoc b/doc/modules/ROOT/pages/logging.adoc index c77fa79c6..cea55f05c 100644 --- a/doc/modules/ROOT/pages/logging.adoc +++ b/doc/modules/ROOT/pages/logging.adoc @@ -8,8 +8,9 @@ = Logging -xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`] -is a complex algorithm, with features like built-in reconnection. +xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`] and +xref:reference:boost/redis/co_connection/run.adoc[`co_connection::run`] +are complex algorithms, with features like built-in reconnection. This can make configuration problems, like a misconfigured hostname, difficult to debug - Boost.Redis will keep retrying to connect to the same hostname over and over. For this reason, Boost.Redis incorporates a lightweight logging solution, and @@ -18,28 +19,66 @@ For this reason, Boost.Redis incorporates a lightweight logging solution, and Logging can be customized by passing a xref:reference:boost/redis/logger.adoc[`logger`] object to the connection's constructor. For example, logging can be disabled by writing: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- asio::io_context ioc; -connection conn {ioc, logger{logger::level::disabled}}; +connection conn{ioc, logger{logger::level::disabled}}; ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +corosio::io_context ctx; +co_connection conn{ctx, logger{logger::level::disabled}}; +---- +-- +======== Every message logged by the library is attached a https://en.wikipedia.org/wiki/Syslog#Severity_level[syslog-like severity] tag (a xref:reference:boost/redis/logger/level.adoc[`logger::level`]). You can filter messages by severity by creating a `logger` with a specific level: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- asio::io_context ioc; // Logs to stderr messages with severity >= level::error. // This will hide all informational output. -connection conn {ioc, logger{logger::level::error}}; +connection conn{ioc, logger{logger::level::error}}; +---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +corosio::io_context ctx; + +// Logs to stderr messages with severity >= level::error. +// This will hide all informational output. +co_connection conn{ctx, logger{logger::level::error}}; ---- +-- +======== The `logger` constructor accepts a `std::function` as second argument. If supplied, Boost.Redis will call this function when logging instead of printing to stderr. This can be used to integrate third-party logging -libraries. See our {site-url}/example/cpp17_spdlog.cpp[spdlog integration example] -for sample code. +libraries. See our spdlog integration examples for sample code: +{site-url}/example/cpp17_spdlog.cpp[cpp17_spdlog.cpp] (Asio) / +{site-url}/example/corosio_spdlog.cpp[corosio_spdlog.cpp] (Corosio). diff --git a/doc/modules/ROOT/pages/pushes.adoc b/doc/modules/ROOT/pages/pushes.adoc index cae1187d6..76bb4b6b2 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,19 +68,84 @@ 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: -* Call xref:reference:boost/redis/basic_connection.adoc[`connection::set_receive_response`] +* Call xref:reference:boost/redis/basic_connection.adoc[`connection::set_receive_response`] / + xref:reference:boost/redis/co_connection/set_receive_response.adoc[`co_connection::set_receive_response`] before any other receive-related calls so that the connection stores incoming pushes in the given object. The library does not copy the response; you must keep it alive for the duration of the receive loop. * Build a request with xref:reference:boost/redis/request.adoc[`request::subscribe`] (or `psubscribe`) and execute it. If the connection drops and is re-established, an equivalent `SUBSCRIBE` is sent automatically. -* Loop while xref:reference:boost/redis/basic_connection.adoc[`connection::will_reconnect`] - is true (i.e. until the connection is cancelled). -* Call xref:reference:boost/redis/basic_connection.adoc[`connection::async_receive2`] +* Loop until the connection is cancelled. + In Asio, gate the loop on xref:reference:boost/redis/basic_connection.adoc[`connection::will_reconnect`]. + In Corosio, run until xref:reference:boost/redis/co_connection/receive.adoc[`co_connection::receive`] + reports an error (e.g. cancellation when the surrounding `when_any` tears down the run loop). +* Call xref:reference:boost/redis/basic_connection.adoc[`connection::async_receive2`] / + xref:reference:boost/redis/co_connection/receive.adoc[`co_connection::receive`] to wait until at least one push is available. This function also participates in push flow control (see <>). * After completion, `resp` holds the raw RESP3 nodes for the received pushes @@ -107,20 +178,27 @@ flow control. After a certain number of pushes have been read and not yet consum stops reading until the application drains the receive buffer. This creates backpressure at the TCP level. -The pending count is reset each time `async_receive2` completes. You must still free +The pending count is reset each time `async_receive2` (Asio) / +`receive` (Corosio) +completes. You must still free memory by calling `resp3::flat_tree::clear()` (or equivalent) on the response, as in the example. -WARNING: Do not call xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`] from within the receiver loop. The response to your request may be behind enough pushes might to trigger the flow control mechanism, causing a deadlock. +WARNING: Do not call +xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`] / +xref:reference:boost/redis/co_connection/exec-00.adoc[`co_connection::exec`] +from within the receiver loop. The response to your request may be behind enough pushes might to trigger the flow control mechanism, causing a deadlock. == Subscribe confirmations Every time you subscribe to a channel, Redis sends a push as a confirmation. These confirmations -are stored in your receive response and cause `async_receive2` to complete like any +are stored in your receive response and cause `async_receive2` (Asio) / +`receive` (Corosio) +to complete like any other push. `push_parser` skips these messages, since they don't contain application-level information. -As a result, *`async_receive2` will complete while the `push_parser` range is empty* +As a result, *the receive call will complete while the `push_parser` range is empty* when only confirmations are received. Your code should handle an empty range correctly. To work with subscribe confirmations or other push shapes, use the raw nodes in @@ -137,6 +215,11 @@ async operation. For example, the following is incorrect: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- auto [ec] = co_await conn->async_receive2(asio::as_tuple); @@ -145,9 +228,29 @@ auto [ec] = co_await conn->async_receive2(asio::as_tuple); co_await websocket.async_send(push_parser(resp.value())); resp.value().clear(); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +auto [ec] = co_await conn.receive(); +// DON'T DO THIS: more pushes may be read while the WebSocket send runs, +// and they are discarded when you clear(), causing a race. +co_await websocket.send(push_parser(resp.value())); +resp.value().clear(); +---- +-- +======== Instead, copy or compose the message without I/O, then clear, then send: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- auto [ec] = co_await conn->async_receive2(asio::as_tuple); @@ -161,6 +264,26 @@ resp.value().clear(); // Now it's OK to suspend the coroutine co_await websocket.async_write(msg); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +auto [ec] = co_await conn.receive(); + +// Compose the WebSocket message without any I/O (no suspension). +// msg must not reference resp. +auto msg = compose_websocket_message(push_parser(resp.value())); + +resp.value().clear(); + +// Now it's OK to suspend the coroutine +co_await websocket.write(msg); +---- +-- +======== == SUBSCRIBE errors 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/modules/ROOT/pages/requests_responses.adoc b/doc/modules/ROOT/pages/requests_responses.adoc index 69728b63c..ae875e916 100644 --- a/doc/modules/ROOT/pages/requests_responses.adoc +++ b/doc/modules/ROOT/pages/requests_responses.adoc @@ -36,7 +36,8 @@ req.push_range("HSET", "key", map); ---- Sending a request to Redis is performed by -xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`] +xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`] / +xref:reference:boost/redis/co_connection/exec-00.adoc[`co_connection::exec`] as already stated. Requests accept a xref:reference:boost/redis/request/config.adoc[`boost::redis::request::config`] object when constructed that dictates how requests are handled in situations like reconnection. The reader is advised to read it carefully. @@ -148,21 +149,55 @@ response< ---- To execute the request and read the response use -xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`]: - +xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`] / +xref:reference:boost/redis/co_connection/exec-00.adoc[`co_connection::exec`]: + +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- co_await conn->async_exec(req, resp); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +co_await conn.exec(req, resp); +---- +-- +======== If the intention is to ignore responses altogether, use xref:reference:boost/redis/ignore.adoc[`ignore`]: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- // Ignores the response co_await conn->async_exec(req, ignore); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +// Ignores the response +co_await conn.exec(req, ignore); +---- +-- +======== Responses that contain nested aggregates or heterogeneous data types will be given special treatment later in xref:#the-general-case[the general case]. As @@ -240,7 +275,9 @@ response< > resp; ---- -For a complete example, see {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]. +For a complete example, see +{site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp] (Asio) / +{site-url}/example/corosio_containers.cpp[corosio_containers.cpp] (Corosio). [#the-general-case] ### The general case @@ -283,12 +320,30 @@ and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost: The vector can be seen as a pre-order view of the response tree. Using it is not different than using other types: +[tabs] +======== +Asio:: ++ +-- [source,cpp] ---- // Receives any RESP3 simple or aggregate data type. boost::redis::generic_response resp; co_await conn->async_exec(req, resp); ---- +-- + +Corosio:: ++ +-- +[source,cpp] +---- +// Receives any RESP3 simple or aggregate data type. +boost::redis::generic_response resp; +co_await conn.exec(req, resp); +---- +-- +======== For example, suppose we want to retrieve a hash data structure from Redis with `HGETALL`, some of the options are diff --git a/doc/modules/ROOT/pages/sentinel.adoc b/doc/modules/ROOT/pages/sentinel.adoc index bd03faff8..1334d95b6 100644 --- a/doc/modules/ROOT/pages/sentinel.adoc +++ b/doc/modules/ROOT/pages/sentinel.adoc @@ -8,9 +8,10 @@ = Sentinel -Boost.Redis supports Redis Sentinel deployments. Sentinel handling -in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`] -automatically connects to Sentinels, resolves the master's address, and connects to the master. +Boost.Redis supports Redis Sentinel deployments. Sentinel handling is built-in: +xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`] and +xref:reference:boost/redis/co_connection/run.adoc[`co_connection::run`] +automatically connect to Sentinels, resolve the master's address, and connect to the master. Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]: @@ -31,9 +32,10 @@ cfg.sentinel.addresses = { cfg.sentinel.master_name = "mymaster"; ---- -Once set, the connection object can be used normally. See our -our {site-url}/example/cpp20_sentinel.cpp[Sentinel example] -for a full program. +Once set, the connection object can be used normally. See our Sentinel examples for full program listings: + +* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp] (Asio) +* {site-url}/example/corosio_sentinel.cpp[corosio_sentinel.cpp] (Corosio) == Connecting to replicas @@ -115,7 +117,7 @@ cfg.sentinel.use_ssl = true; // Applies to Sentinels == Sentinel algorithm -This section details how `async_run` interacts with Sentinel. +This section details how `connection::async_run` and `co_connection::run` interact with Sentinel. Most of the algorithm follows https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines]. Some of these details may vary between library versions. diff --git a/doc/modules/ROOT/pages/serialization.adoc b/doc/modules/ROOT/pages/serialization.adoc index b95c5c2ef..328de350f 100644 --- a/doc/modules/ROOT/pages/serialization.adoc +++ b/doc/modules/ROOT/pages/serialization.adoc @@ -22,5 +22,7 @@ void boost_redis_from_bulk(mystruct& u, node_view const& node, boost::system::er These functions are accessed over ADL and therefore they must be imported in the global namespace by the user. The following examples might be of interest: -* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: serializes and parses JSON objects. -* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: serializes and parses https://protobuf.dev/[protobuf] objects. +* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp] (Asio) / + {site-url}/example/corosio_json.cpp[corosio_json.cpp] (Corosio): + serializes and parses JSON objects. +* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: serializes and parses https://protobuf.dev/[protobuf] objects (Asio only). 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 4f8ee17a0..f0c64acb3 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -5,8 +5,9 @@ "packages": { "": { "dependencies": { - "@cppalliance/antora-downloads-extension": "^0.0.2", + "@asciidoctor/tabs": "^1.0.0-beta.6", "@cppalliance/antora-cpp-reference-extension": "^0.1.0", + "@cppalliance/antora-downloads-extension": "^0.0.2", "antora": "^3.1.10" } }, @@ -311,6 +312,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 bb5bd7030..1685df841 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@asciidoctor/tabs": "^1.0.0-beta.6", "@cppalliance/antora-downloads-extension": "^0.0.2", "@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 d3592a29f..a7d09e0ad 100644 --- a/doc/redis-playbook.yml +++ b/doc/redis-playbook.yml @@ -26,6 +26,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 54548bba1..5b89008fa 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,11 +1,9 @@ add_library(boost_redis_examples_main STATIC main.cpp) -target_link_libraries(boost_redis_examples_main PRIVATE boost_redis_project_options boost_redis_src) +target_link_libraries(boost_redis_examples_main PUBLIC boost_redis_asio_src) function(boost_redis_make_example EXAMPLE_NAME) set(EXE_NAME "boost_redis_${EXAMPLE_NAME}") add_executable(${EXE_NAME} ${EXAMPLE_NAME}.cpp) - target_link_libraries(${EXE_NAME} PRIVATE boost_redis_src) - target_link_libraries(${EXE_NAME} PRIVATE boost_redis_project_options) if (ARGN) target_link_libraries(${EXE_NAME} PRIVATE ${ARGN}) endif() @@ -18,20 +16,34 @@ function(boost_redis_make_testable_example EXAMPLE_NAME) add_dependencies(tests ${EXE_NAME}) endfunction() -boost_redis_make_testable_example(cpp17_intro) -boost_redis_make_testable_example(cpp17_intro_sync) +boost_redis_make_testable_example(cpp17_intro boost_redis_asio_src) +boost_redis_make_testable_example(cpp17_intro_sync boost_redis_asio_src) -boost_redis_make_testable_example(cpp20_intro boost_redis_examples_main) -boost_redis_make_testable_example(cpp20_containers boost_redis_examples_main) -boost_redis_make_testable_example(cpp20_json boost_redis_examples_main Boost::json Boost::container_hash) -boost_redis_make_testable_example(cpp20_unix_sockets boost_redis_examples_main) -boost_redis_make_testable_example(cpp20_timeouts boost_redis_examples_main) -boost_redis_make_testable_example(cpp20_sentinel boost_redis_examples_main) +boost_redis_make_testable_example(cpp20_intro boost_redis_examples_main) +boost_redis_make_testable_example(cpp20_containers boost_redis_examples_main) +boost_redis_make_testable_example(cpp20_json boost_redis_examples_main Boost::json Boost::container_hash) +boost_redis_make_testable_example(cpp20_unix_sockets boost_redis_examples_main) +boost_redis_make_testable_example(cpp20_timeouts boost_redis_examples_main) +boost_redis_make_testable_example(cpp20_sentinel boost_redis_examples_main) -boost_redis_make_example(cpp20_subscriber boost_redis_examples_main) -boost_redis_make_example(cpp20_streams boost_redis_examples_main) -boost_redis_make_example(cpp20_echo_server boost_redis_examples_main) -boost_redis_make_example(cpp20_intro_tls boost_redis_examples_main) +boost_redis_make_example(cpp20_subscriber boost_redis_examples_main) +boost_redis_make_example(cpp20_streams boost_redis_examples_main) +boost_redis_make_example(cpp20_echo_server boost_redis_examples_main) +boost_redis_make_example(cpp20_intro_tls boost_redis_examples_main) + +if (BOOST_REDIS_COROSIO_TESTS) + boost_redis_make_testable_example(corosio_intro boost_redis_corosio_src) + boost_redis_make_testable_example(corosio_containers boost_redis_corosio_src) + boost_redis_make_testable_example(corosio_json boost_redis_corosio_src Boost::json Boost::container_hash) + boost_redis_make_testable_example(corosio_unix_sockets boost_redis_corosio_src) + boost_redis_make_testable_example(corosio_timeouts boost_redis_corosio_src) + boost_redis_make_testable_example(corosio_sentinel boost_redis_corosio_src) + + boost_redis_make_example(corosio_subscriber boost_redis_corosio_src) + boost_redis_make_example(corosio_streams boost_redis_corosio_src) + boost_redis_make_example(corosio_echo_server boost_redis_corosio_src) + boost_redis_make_example(corosio_intro_tls boost_redis_corosio_src) +endif() # We test the protobuf example only on gcc. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -51,7 +63,10 @@ endif() # We build and test the spdlog integration example only if the library is found find_package(spdlog) if (spdlog_FOUND) - boost_redis_make_testable_example(cpp17_spdlog spdlog::spdlog) + boost_redis_make_testable_example(cpp17_spdlog spdlog::spdlog boost_redis_asio_src) + if (BOOST_REDIS_COROSIO_TESTS) + boost_redis_make_testable_example(corosio_spdlog spdlog::spdlog boost_redis_corosio_src) + endif() 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..1b1cb3e06 --- /dev/null +++ b/include/boost/redis/co_connection.hpp @@ -0,0 +1,424 @@ +// +// 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 +#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 + requires( + !std::same_as && + capy::Executor // if Executor goes before same_as, concept recursion happens + ) + 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; + + co_connection(const co_connection&) = delete; + + /** @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; + + co_connection& operator=(const co_connection&) = delete; + + /// 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 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 5065b3645..e5c2295d2 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()) { @@ -304,7 +321,7 @@ struct exec_one_op { conn_->st_.mpx, 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 +363,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 +402,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 +417,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 +440,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 +453,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 +485,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 +524,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/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 84a54a438..3b063b7d6 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 @@ -63,7 +63,7 @@ class exec_one_fsm { multiplexer& mpx, 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..d81525482 --- /dev/null +++ b/include/boost/redis/detail/flow_controller.hpp @@ -0,0 +1,93 @@ +/* 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 { + +// Allows controlling the received pushes. This is a substitute +// for the old channel-based implementation. +// The object stores the number of pending bytes. +// The receiver calls take(), which marks the object as empty. take() blocks if no bytes are pending. +// The reader calls try_put() and put(), which increases the pending byte count. +// When the pending byte count raises above the configured max bytes, put() blocks. +// The actual pending bytes may temporarily exceed the configured max bytes, but subsequent put() calls will block. +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) + : max_bytes_(max_bytes) + { + room_available_.set(); + BOOST_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 {}; + } + + // Tries to put bytes into the object, without blocking. + // Returns true if the operation succeeded. + // Otherwise, does nothing and returns false. + 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. This avoids problems in the theoretical + // case of reading a very big push. + // The following messages will wait + pending_bytes_ += bytes; + if (pending_bytes_ >= max_bytes_) + room_available_.clear(); + bytes_available_.set(); + + return true; + } + + // Puts bytes into the object. Blocks if the object is full. + 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 {}; + } + + // Exposed for testing + std::size_t pending_bytes() const { return pending_bytes_; } +}; + +} // 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/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.hpp b/include/boost/redis/impl/co_connect.hpp new file mode 100644 index 000000000..2f48fc5b3 --- /dev/null +++ b/include/boost/redis/impl/co_connect.hpp @@ -0,0 +1,145 @@ +// +// 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_HPP +#define BOOST_REDIS_CO_CONNECT_HPP + +#include +#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, const corosio::resolver_results& 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); + } + } + } +}; + +// Templatized for testing purposes. +// StreamImpl should hold members to establish connections using +// any of the supported transports. +// Performs connection establishment, and outputs a stream to 'out'. +// The resulting stream should be non-owning, pointing into impl's data members, allowing re-use. + +template +capy::io_task<> co_connect( + StreamImpl& impl, + const connect_params& params, + buffered_logger& lgr, + capy::any_stream& out) +{ + // gcc-15 emits a bogus diagnostic for structured bindings here +#if defined(BOOST_GCC) && BOOST_GCC >= 150000 && BOOST_GCC < 160000 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + auto type = params.addr.type(); + + if (type == transport_type::unix_socket) { + // Setup + impl.setup_unix(out); + + // Actual connect + auto [ec] = co_await impl.unix_connect(params); + if (ec) { + log_info(lgr, "Connect: UNIX socket connect failed: ", system::error_code(ec)); + co_return {ec}; + } + log_debug(lgr, "Connect: UNIX socket connect succeeded"); + + // Done + co_return {}; + + } else { + // TCP (with or without TLS) + if (type == transport_type::tcp_tls) + impl.setup_tcp_tls(out); + else + impl.setup_tcp(out); + + // Resolve names + auto [ec_resolve, endpoints] = co_await impl.tcp_resolve(params); + if (ec_resolve) { + log_info(lgr, "Connect: hostname resolution failed: ", system::error_code(ec_resolve)); + co_return {ec_resolve}; + } + log_debug(lgr, "Connect: hostname resolution results: ", endpoints); + + // Now connect to the endpoints returned by the resolver + auto [ec_connect, selected_endpoint] = co_await impl.tcp_connect(params, endpoints); + if (ec_connect) { + log_info(lgr, "Connect: TCP connect failed: ", system::error_code(ec_connect)); + co_return {ec_connect}; + } + log_debug(lgr, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint); + + // If using TLS, perform the handshake + if (type == transport_type::tcp_tls) { + // TLS handshake + auto [ec_handshake] = co_await impl.tls_handshake(params); + if (ec_handshake) { + log_info(lgr, "Connect: SSL handshake failed: ", system::error_code(ec_handshake)); + co_return {ec_handshake}; + } + log_debug(lgr, "Connect: SSL handshake succeeded"); + } + + // Done + co_return {}; + } +#if defined(BOOST_GCC) && BOOST_GCC >= 150000 && BOOST_GCC < 160000 +#pragma GCC diagnostic pop +#endif +} + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp new file mode 100644 index 000000000..5c924df60 --- /dev/null +++ b/include/boost/redis/impl/co_connection.ipp @@ -0,0 +1,440 @@ +// +// 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 +#include + +namespace boost::redis { +namespace detail { + +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); +} + +// An object suitable to be passed to co_connect +class stream_impl { + 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_; + + void setup_tcp_impl() + { + // 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_); + } + +public: + explicit stream_impl(capy::execution_context& ctx, corosio::tls_context tls_ctx) + : ctx_(ctx) + , tls_ctx_(std::move(tls_ctx)) + { } + + void setup_unix(capy::any_stream& out) + { + 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_); + } + out = capy::any_stream(&*unix_); + } + + void setup_tcp(capy::any_stream& out) + { + setup_tcp_impl(); + out = capy::any_stream(&tcp_->sock); + } + + void setup_tcp_tls(capy::any_stream& out) + { + setup_tcp_impl(); + if (tls_.has_value()) + tls_->reset(); + else + tls_.emplace(capy::any_stream(&tcp_->sock), tls_ctx_); + out = capy::any_stream(&*tls_); + } + + auto unix_connect(const connect_params& params) + { + return capy::timeout( + unix_->connect(corosio::local_endpoint(params.addr.unix_socket())), + params.connect_timeout); + } + + auto tcp_resolve(const connect_params& params) + { + return capy::timeout( + tcp_->resolv.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), + params.resolve_timeout); + } + + auto tcp_connect(const connect_params& params, const corosio::resolver_results& results) + { + // all() prevents corosio from copying results + return capy::timeout( + corosio::connect(tcp_->sock, std::ranges::views::all(results)), + params.connect_timeout); + } + + auto tls_handshake(const connect_params& params) + { + return capy::timeout( + tls_->handshake(corosio::tls_stream::handshake_type::client), + params.ssl_handshake_timeout); + } +}; + +struct co_connection_impl { + capy::async_event run_cancelled_event_; + stream_impl stream_impl_; + capy::any_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_impl_{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<> connect(const connect_params& params) + { + return co_connect(stream_impl_, params, st_.logger, stream_); + } + + 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 {std::error_code(act.error())}; + } + } + } + + capy::io_task<> receive() { return controller_.take(); } + + capy::io_task<> exec_one(const request& req, any_adapter resp) + { + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + auto& mpx = st_.mpx; + + // First invocation + auto act = fsm.resume(st_.mpx, system::error_code(), 0u, cancellation_type::none); + + while (true) { + switch (act.type) { + case exec_one_action_type::done: co_return {std::error_code(act.ec)}; + case exec_one_action_type::write: + { + auto [ec, bytes] = co_await capy::write(stream_, capy::make_buffer(req.payload())); + act = fsm.resume(mpx, 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 = mpx.get_read_buffer().get_prepared(); + auto [ec, bytes] = co_await stream_.read_some( + capy::mutable_buffer(buff.data(), buff.size())); + act = fsm.resume(mpx, 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 {std::error_code(act.error())}; + case sentinel_action::type::connect: + { + auto [ec] = co_await connect( + make_sentinel_connect_params(st_.cfg, act.connect_addr())); + 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 {{}, std::error_code(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: + { + // A zero timeout value means "no timeout" + auto timeout = act.timeout(); + auto expiry = timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; + writer_cv_.expires_at(expiry); + 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 {{}, std::error_code(act.error())}; + } + } + } + + capy::io_task<> run(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_.mpx.set_config(cfg); + st_.cfg = std::move(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 {std::error_code(act.ec)}; + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve()).ec; break; + case run_action_type::connect: + ec = (co_await connect(make_run_connect_params(st_))).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 cfg) { return impl_->run(std::move(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 503739865..95cbca212 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( multiplexer& mpx, 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 f548b2ab5..e7bb23f50 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 @@ -248,7 +248,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_)); @@ -281,7 +281,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/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..68ae0313c --- /dev/null +++ b/include/boost/redis/src/asio.hpp @@ -0,0 +1,9 @@ +/* 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 diff --git a/include/boost/redis/src/corosio.hpp b/include/boost/redis/src/corosio.hpp new file mode 100644 index 000000000..a40e4650c --- /dev/null +++ b/include/boost/redis/src/corosio.hpp @@ -0,0 +1,7 @@ +/* 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 diff --git a/include/boost/redis/src/proto.hpp b/include/boost/redis/src/proto.hpp new file mode 100644 index 000000000..cd44060c9 --- /dev/null +++ b/include/boost/redis/src/proto.hpp @@ -0,0 +1,25 @@ +/* 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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 385914b63..842392e0b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,11 @@ +# Report whether we're building the Corosio tests or not, for CI traceability +if (NOT BOOST_REDIS_COROSIO_API) + message(STATUS "Skipping Corosio tests and examples because BOOST_REDIS_COROSIO_API is not defined") +endif() + # Common utilities add_library(boost_redis_project_options INTERFACE) -target_link_libraries(boost_redis_project_options INTERFACE boost_redis) if (MSVC) # C4459: name hides outer scope variable is issued by Asio target_compile_options(boost_redis_project_options INTERFACE /bigobj /W4 /wd4459) @@ -10,70 +14,125 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL target_compile_options(boost_redis_project_options INTERFACE -Wall -Wextra) 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_src STATIC boost_redis_proto.cpp) +target_link_libraries(boost_redis_proto_src PUBLIC + boost_redis_proto + boost_redis_project_options +) + +add_library(boost_redis_asio_src STATIC boost_redis_asio.cpp) +target_link_libraries(boost_redis_asio_src PUBLIC + boost_redis + boost_redis_proto_src +) + +if (BOOST_REDIS_COROSIO_API) + add_library(boost_redis_corosio_src STATIC boost_redis_corosio.cpp) + target_link_libraries(boost_redis_corosio_src PUBLIC + boost_redis_corosio + boost_redis_proto_src + ) +endif() # Test utils -add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp) -target_link_libraries(boost_redis_tests_common PUBLIC boost_redis_project_options boost_redis_src) -target_compile_definitions(boost_redis_tests_common INTERFACE BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns +add_library(boost_redis_tests_proto STATIC common.cpp sansio_utils.cpp) +target_link_libraries(boost_redis_tests_proto PUBLIC boost_redis_proto_src) +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 + boost_redis_asio_src + boost_redis_tests_proto +) + +if (BOOST_REDIS_COROSIO_API) + add_library(boost_redis_tests_corosio STATIC corosio_common.cpp) + target_link_libraries(boost_redis_tests_corosio PUBLIC + boost_redis_corosio_src + boost_redis_tests_proto + ) +endif() function(boost_redis_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_tests_common) + if (ARGN) + target_link_libraries(${EXE_NAME} PRIVATE ${ARGN}) + endif() add_test(NAME ${EXE_NAME} COMMAND ${EXE_NAME}) add_dependencies(tests ${EXE_NAME}) endfunction() # Unit tests -boost_redis_make_test(test_low_level) -boost_redis_make_test(test_request) -boost_redis_make_test(test_serialization) -boost_redis_make_test(test_low_level_sync_sans_io) -boost_redis_make_test(test_any_adapter) -boost_redis_make_test(test_log_to_file) -boost_redis_make_test(test_conn_logging) -boost_redis_make_test(test_exec_fsm) -boost_redis_make_test(test_exec_one_fsm) -boost_redis_make_test(test_writer_fsm) -boost_redis_make_test(test_reader_fsm) -boost_redis_make_test(test_connect_fsm) -boost_redis_make_test(test_sentinel_resolve_fsm) -boost_redis_make_test(test_receive_fsm) -boost_redis_make_test(test_run_fsm) -boost_redis_make_test(test_compose_setup_request) -boost_redis_make_test(test_setup_adapter) -boost_redis_make_test(test_multiplexer) -boost_redis_make_test(test_parse_sentinel_response) -boost_redis_make_test(test_update_sentinel_list) -boost_redis_make_test(test_flat_tree) -boost_redis_make_test(test_generic_flat_response) -boost_redis_make_test(test_read_buffer) -boost_redis_make_test(test_subscription_tracker) -boost_redis_make_test(test_push_parser) +boost_redis_make_test(test_low_level boost_redis_tests_proto) +boost_redis_make_test(test_request boost_redis_tests_proto) +boost_redis_make_test(test_serialization boost_redis_tests_proto) +boost_redis_make_test(test_low_level_sync_sans_io boost_redis_tests_proto) +boost_redis_make_test(test_any_adapter boost_redis_tests_proto) +boost_redis_make_test(test_log_to_file boost_redis_tests_proto) +boost_redis_make_test(test_exec_fsm boost_redis_tests_proto) +boost_redis_make_test(test_exec_one_fsm boost_redis_tests_proto) +boost_redis_make_test(test_writer_fsm boost_redis_tests_proto Boost::asio) +boost_redis_make_test(test_reader_fsm boost_redis_tests_proto Boost::asio) +boost_redis_make_test(test_sentinel_resolve_fsm boost_redis_tests_proto Boost::asio) +boost_redis_make_test(test_run_fsm boost_redis_tests_proto Boost::asio) +boost_redis_make_test(test_compose_setup_request boost_redis_tests_proto) +boost_redis_make_test(test_setup_adapter boost_redis_tests_proto) +boost_redis_make_test(test_multiplexer boost_redis_tests_proto) +boost_redis_make_test(test_parse_sentinel_response boost_redis_tests_proto) +boost_redis_make_test(test_update_sentinel_list boost_redis_tests_proto) +boost_redis_make_test(test_flat_tree boost_redis_tests_proto) +boost_redis_make_test(test_generic_flat_response boost_redis_tests_proto) +boost_redis_make_test(test_read_buffer boost_redis_tests_proto) +boost_redis_make_test(test_subscription_tracker boost_redis_tests_proto) +boost_redis_make_test(test_push_parser boost_redis_tests_proto) + +boost_redis_make_test(test_conn_logging boost_redis_tests_asio) +boost_redis_make_test(test_connect_fsm boost_redis_tests_asio) +boost_redis_make_test(test_receive_fsm boost_redis_tests_asio) + +if (BOOST_REDIS_COROSIO_API) + boost_redis_make_test(test_co_logging boost_redis_tests_corosio) + boost_redis_make_test(test_flow_controller boost_redis_tests_corosio) + boost_redis_make_test(test_co_connect boost_redis_tests_corosio) + boost_redis_make_test(test_mix_asio_corosio boost_redis boost_redis_corosio) +endif() # Tests that require a real Redis server if (BOOST_REDIS_INTEGRATION_TESTS) - boost_redis_make_test(test_conn_quit) - boost_redis_make_test(test_conn_exec_retry) - boost_redis_make_test(test_conn_exec_error) - boost_redis_make_test(test_run) - boost_redis_make_test(test_conn_run_cancel) - boost_redis_make_test(test_conn_check_health) - boost_redis_make_test(test_conn_exec) - boost_redis_make_test(test_conn_push) - boost_redis_make_test(test_conn_push2) - boost_redis_make_test(test_conn_monitor) - boost_redis_make_test(test_conn_reconnect) - boost_redis_make_test(test_conn_exec_cancel) - boost_redis_make_test(test_conn_echo_stress) - boost_redis_make_test(test_conn_move) - boost_redis_make_test(test_conn_setup) - boost_redis_make_test(test_issue_50) - boost_redis_make_test(test_conversions) - boost_redis_make_test(test_conn_tls) - boost_redis_make_test(test_unix_sockets) - boost_redis_make_test(test_conn_cancel_after) - boost_redis_make_test(test_conn_sentinel) + boost_redis_make_test(test_conn_quit boost_redis_tests_asio) + boost_redis_make_test(test_conn_exec_retry boost_redis_tests_asio) + boost_redis_make_test(test_conn_exec_error boost_redis_tests_asio) + boost_redis_make_test(test_run boost_redis_tests_asio) + boost_redis_make_test(test_conn_run_cancel boost_redis_tests_asio) + boost_redis_make_test(test_conn_check_health boost_redis_tests_asio) + boost_redis_make_test(test_conn_exec boost_redis_tests_asio) + boost_redis_make_test(test_conn_push boost_redis_tests_asio) + boost_redis_make_test(test_conn_push2 boost_redis_tests_asio) + boost_redis_make_test(test_conn_monitor boost_redis_tests_asio) + boost_redis_make_test(test_conn_reconnect boost_redis_tests_asio) + boost_redis_make_test(test_conn_exec_cancel boost_redis_tests_asio) + boost_redis_make_test(test_conn_echo_stress boost_redis_tests_asio) + boost_redis_make_test(test_conn_move boost_redis_tests_asio) + boost_redis_make_test(test_conn_setup boost_redis_tests_asio) + boost_redis_make_test(test_issue_50 boost_redis_tests_asio) + boost_redis_make_test(test_conversions boost_redis_tests_asio) + boost_redis_make_test(test_conn_tls boost_redis_tests_asio) + boost_redis_make_test(test_unix_sockets boost_redis_tests_asio) + boost_redis_make_test(test_conn_cancel_after boost_redis_tests_asio) + boost_redis_make_test(test_conn_sentinel boost_redis_tests_asio) + + if (BOOST_REDIS_COROSIO_API) + boost_redis_make_test(test_co_run_cancel boost_redis_tests_corosio) + boost_redis_make_test(test_co_check_health boost_redis_tests_corosio) + boost_redis_make_test(test_co_push2 boost_redis_tests_corosio) + boost_redis_make_test(test_co_exec_cancel boost_redis_tests_corosio) + boost_redis_make_test(test_co_move boost_redis_tests_corosio) + boost_redis_make_test(test_co_setup boost_redis_tests_corosio) + boost_redis_make_test(test_co_tls boost_redis_tests_corosio) + boost_redis_make_test(test_co_unix_sockets boost_redis_tests_corosio) + boost_redis_make_test(test_co_sentinel boost_redis_tests_corosio) + endif() + endif() \ No newline at end of file diff --git a/test/Jamfile b/test/Jamfile index 3ddb1be79..09515b430 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -4,9 +4,31 @@ import config : requires ; import ac ; import indirect ; -# Configure openssl if it hasn't been done yet +# Configure openssl and zlib if it hasn't been done yet using openssl ; +# Use these requirements across all tests +project : requirements + msvc:"/bigobj" + windows:_WIN32_WINNT=0x0601 + _CRT_SECURE_NO_WARNINGS=1 # suppress MSVC warnings + [ requires + cxx14_constexpr + cxx14_generic_lambdas + cxx14_initialized_lambda_captures + cxx14_aggregate_nsdmi + cxx14_return_type_deduction + cxx17_hdr_charconv + cxx17_hdr_optional + cxx17_hdr_string_view + cxx17_hdr_variant + cxx17_std_apply + cxx17_structured_bindings + ] + [ ac.check-library /openssl//ssl : /openssl//ssl/shared : no ] + [ ac.check-library /openssl//crypto : /openssl//crypto/shared : no ] +; + # Provide a way to fail the build if OpenSSL is not found - used by CIs rule do_fail_impl ( a * ) { @@ -23,83 +45,87 @@ alias fail_if_no_openssl explicit fail_if_no_openssl ; -# Use these requirements as both regular and usage requirements across all tests -local requirements = - /boost/redis//boost_redis - BOOST_ASIO_NO_DEPRECATED=1 - BOOST_ASIO_DISABLE_BOOST_ARRAY=1 - BOOST_ASIO_DISABLE_BOOST_BIND=1 - BOOST_ASIO_DISABLE_BOOST_DATE_TIME=1 - BOOST_ASIO_DISABLE_BOOST_REGEX=1 +# Helper libraries +lib redis_tests_proto + : + boost_redis_proto.cpp + common.cpp + sansio_utils.cpp + /boost/redis//boost_redis_proto + : usage-requirements BOOST_ALLOW_DEPRECATED=1 # we need to test deprecated fns - _CRT_SECURE_NO_WARNINGS=1 # suppress MSVC warnings - msvc:"/bigobj" - windows:_WIN32_WINNT=0x0601 - [ requires - cxx14_constexpr - cxx14_generic_lambdas - cxx14_initialized_lambda_captures - cxx14_aggregate_nsdmi - cxx14_return_type_deduction - cxx17_hdr_charconv - cxx17_hdr_optional - cxx17_hdr_string_view - cxx17_hdr_variant - cxx17_std_apply - cxx17_structured_bindings - ] - [ ac.check-library /openssl//ssl : /openssl//ssl/shared : no ] - [ ac.check-library /openssl//crypto : /openssl//crypto/shared : no ] - ; +; + +explicit redis_tests_proto ; + +local requirements_asio = + BOOST_ASIO_NO_DEPRECATED=1 + BOOST_ASIO_DISABLE_BOOST_ARRAY=1 + BOOST_ASIO_DISABLE_BOOST_BIND=1 + BOOST_ASIO_DISABLE_BOOST_DATE_TIME=1 + BOOST_ASIO_DISABLE_BOOST_REGEX=1 +; +lib redis_tests_asio + : + boost_redis_asio.cpp + asio_common.cpp + redis_tests_proto + /boost/redis//boost_redis +; -# Helper library -lib redis_test_common +explicit redis_tests_asio ; + +lib redis_tests_corosio : - boost_redis.cpp - common.cpp - sansio_utils.cpp - : requirements $(requirements) - : usage-requirements $(requirements) + boost_redis_corosio.cpp + corosio_common.cpp + redis_tests_proto + /boost/redis//boost_redis_corosio/off ; +explicit redis_tests_corosio ; + # B2 runs tests in parallel, and some tests rely on having exclusive # access to a Redis server, so we only run the ones that don't require a DB server. -local tests = - test_low_level - test_request - test_serialization - test_low_level_sync_sans_io - test_any_adapter - test_log_to_file - test_conn_logging - test_exec_fsm - test_exec_one_fsm - test_writer_fsm - test_reader_fsm - test_sentinel_resolve_fsm - test_receive_fsm - test_run_fsm - test_connect_fsm - test_compose_setup_request - test_setup_adapter - test_multiplexer - test_parse_sentinel_response - test_update_sentinel_list - test_flat_tree - test_generic_flat_response - test_read_buffer - test_subscription_tracker - test_push_parser -; - -# Build and run the tests -for local test in $(tests) +rule make_test ( + test_name : + libs * : +) { run - $(test).cpp - redis_test_common/static - : target-name $(test) + $(test_name).cpp + $(libs)/static + : target-name $(test_name) ; } +make_test test_low_level : redis_tests_proto ; +make_test test_request : redis_tests_proto ; +make_test test_serialization : redis_tests_proto ; +make_test test_low_level_sync_sans_io : redis_tests_proto ; +make_test test_any_adapter : redis_tests_proto ; +make_test test_log_to_file : redis_tests_proto ; +make_test test_conn_logging : redis_tests_asio ; +make_test test_co_logging : redis_tests_corosio ; +make_test test_exec_fsm : redis_tests_proto ; +make_test test_exec_one_fsm : redis_tests_proto ; +make_test test_writer_fsm : redis_tests_proto /boost/capy//boost_capy ; +make_test test_reader_fsm : redis_tests_proto ; +make_test test_connect_fsm : redis_tests_asio ; +make_test test_sentinel_resolve_fsm : redis_tests_proto ; +make_test test_receive_fsm : redis_tests_asio ; +make_test test_run_fsm : redis_tests_proto ; +make_test test_compose_setup_request : redis_tests_proto ; +make_test test_setup_adapter : redis_tests_proto ; +make_test test_multiplexer : redis_tests_proto ; +make_test test_parse_sentinel_response : redis_tests_proto ; +make_test test_update_sentinel_list : redis_tests_proto ; +make_test test_flat_tree : redis_tests_proto ; +make_test test_generic_flat_response : redis_tests_proto ; +make_test test_read_buffer : redis_tests_proto ; +make_test test_subscription_tracker : redis_tests_proto ; +make_test test_push_parser : redis_tests_proto ; +make_test test_flow_controller : redis_tests_corosio ; +make_test test_co_connect : redis_tests_corosio ; +make_test test_mix_asio_corosio : /boost/redis//boost_redis /boost/redis//boost_redis_corosio/off ; 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/cmake_b2_corosio_test/CMakeLists.txt b/test/cmake_b2_corosio_test/CMakeLists.txt new file mode 100644 index 000000000..877e7f8d8 --- /dev/null +++ b/test/cmake_b2_corosio_test/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.5...3.22) + +project(boost_redis_corosio_b2_test LANGUAGES CXX) + +find_package(Boost REQUIRED COMPONENTS headers capy corosio corosio_openssl) + +add_executable(main main.cpp) +target_link_libraries(main PRIVATE + Boost::headers + Boost::capy + Boost::corosio + Boost::corosio_openssl +) + +include(CTest) +add_test(NAME main COMMAND main) diff --git a/test/cmake_b2_corosio_test/main.cpp b/test/cmake_b2_corosio_test/main.cpp new file mode 100644 index 000000000..4b560a017 --- /dev/null +++ b/test/cmake_b2_corosio_test/main.cpp @@ -0,0 +1,20 @@ +// +// 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 + +int main() +{ + boost::corosio::io_context ctx; + boost::redis::co_connection conn(ctx); + return 0; +} diff --git a/test/cmake_install_corosio_test/CMakeLists.txt b/test/cmake_install_corosio_test/CMakeLists.txt new file mode 100644 index 000000000..8515b52cb --- /dev/null +++ b/test/cmake_install_corosio_test/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.5...3.22) + +project(cmake_install_corosio_test LANGUAGES CXX) + +find_package(boost_redis_corosio REQUIRED) + +add_executable(main main.cpp) +target_link_libraries(main PRIVATE Boost::redis_corosio) + +include(CTest) +add_test(main main) + +add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure -C $) diff --git a/test/cmake_install_corosio_test/main.cpp b/test/cmake_install_corosio_test/main.cpp new file mode 100644 index 000000000..4b560a017 --- /dev/null +++ b/test/cmake_install_corosio_test/main.cpp @@ -0,0 +1,20 @@ +// +// 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 + +int main() +{ + boost::corosio::io_context ctx; + boost::redis::co_connection conn(ctx); + return 0; +} diff --git a/test/cmake_subdir_corosio_test/CMakeLists.txt b/test/cmake_subdir_corosio_test/CMakeLists.txt new file mode 100644 index 000000000..6f849b3c1 --- /dev/null +++ b/test/cmake_subdir_corosio_test/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.5...3.22) + +project(cmake_subdir_corosio_test LANGUAGES CXX) + +set(BOOST_INCLUDE_LIBRARIES redis) +set(BOOST_REDIS_COROSIO_API ON) + +# Build our dependencies, so the targets Boost::xxx are defined +add_subdirectory(../../../.. boostorg/boost) + +add_executable(main main.cpp) +target_link_libraries(main PRIVATE Boost::redis_corosio) + +include(CTest) +add_test(main main) + +add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure -C $) diff --git a/test/cmake_subdir_corosio_test/main.cpp b/test/cmake_subdir_corosio_test/main.cpp new file mode 100644 index 000000000..4b560a017 --- /dev/null +++ b/test/cmake_subdir_corosio_test/main.cpp @@ -0,0 +1,20 @@ +// +// 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 + +int main() +{ + boost::corosio::io_context ctx; + boost::redis::co_connection conn(ctx); + return 0; +} diff --git a/test/common.cpp b/test/common.cpp index e13de33a6..11763985e 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,42 +1,17 @@ #include -#include -#include -#include #include #include "common.hpp" #include #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,11 @@ 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() << ')'; +} + +condition_wrapper canceled_condition() { return {std::errc::operation_canceled}; } \ No newline at end of file diff --git a/test/common.hpp b/test/common.hpp index 6c7547baa..cfa64b2bc 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -1,46 +1,57 @@ #pragma once -#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 -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..72cc900a5 --- /dev/null +++ b/test/corosio_common.cpp @@ -0,0 +1,81 @@ +// +// 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 "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; +} + +condition_wrapper boost::redis::test::capy_canceled_condition() { return {capy::cond::canceled}; } diff --git a/test/corosio_common.hpp b/test/corosio_common.hpp new file mode 100644 index 000000000..78a860204 --- /dev/null +++ b/test/corosio_common.hpp @@ -0,0 +1,33 @@ +// +// 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 + +#include "common.hpp" + +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); + +// TODO: this should be std::errc::operation_canceled (i.e. canceled_cond()) +// https://github.com/cppalliance/capy/issues/267 +condition_wrapper capy_canceled_condition(); + +} // 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..f050da206 --- /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, capy_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, capy_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, capy_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, capy_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, capy_canceled_condition()); + co_return {}; + }; + + auto run2_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn2.run(cfg); + BOOST_TEST_EQ(ec, capy_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 == capy_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 == capy_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_connect.cpp b/test/test_co_connect.cpp new file mode 100644 index 000000000..abdbfd1cc --- /dev/null +++ b/test/test_co_connect.cpp @@ -0,0 +1,383 @@ +// +// 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 "corosio_common.hpp" +#include "sansio_utils.hpp" + +#include +#include +#include +#include +#include + +using namespace boost::redis; +using namespace boost::redis::test; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using detail::co_connect; +using detail::buffered_logger; +using detail::transport_type; +using detail::any_address_view; +using detail::connect_params; +using boost::system::error_code; +using namespace std::chrono_literals; + +// Operators +static const char* to_string(transport_type type) +{ + switch (type) { + case transport_type::tcp: return "transport_type::tcp"; + case transport_type::tcp_tls: return "transport_type::tcp_tls"; + case transport_type::unix_socket: return "transport_type::unix_socket"; + default: return ""; + } +} + +namespace boost::redis::detail { + +std::ostream& operator<<(std::ostream& os, transport_type type) { return os << to_string(type); } + +} // namespace boost::redis::detail + +namespace { + +// Parses an endpoint and checks its return value +corosio::endpoint make_endpoint(std::string_view ep) +{ + corosio::endpoint res; + auto ec = corosio::parse_endpoint(ep, res); + if (!BOOST_TEST_EQ(ec, std::error_code())) + std::cerr << " Endpoint was: " << ep << std::endl; + return res; +} + +// Easier way to create a connect_params +connect_params make_connect_params(any_address_view addr) +{ + return { + .addr = addr, + .resolve_timeout = 10s, + .connect_timeout = 10s, + .ssl_handshake_timeout = 10s, + }; +} + +// Mocking infrastructure +struct num_calls { + int setup_unix{}, setup_tcp{}, setup_tcp_tls{}, unix_connect{}, tcp_resolve{}, tcp_connect{}, + tls_handshake{}; + + friend bool operator==(const num_calls&, const num_calls&) = default; +}; + +std::ostream& operator<<(std::ostream& os, const num_calls& v) +{ + return os << "{ .setup_unix=" << v.setup_unix << ", .setup_tcp=" << v.setup_tcp + << ", .setup_tcp_tls=" << v.setup_tcp_tls << ", .unix_connect=" << v.unix_connect + << ", .tcp_resolve=" << v.tcp_resolve << ", .tcp_connect=" << v.tcp_connect + << ", .tls_handshake=" << v.tls_handshake << " }"; +} + +struct return_value { + std::error_code unix_connect{}, tcp_resolve{}, tcp_connect{}, tls_handshake{}; +}; + +struct mock_impl { + num_calls calls; + return_value retval; + + void setup_unix(capy::any_stream&) { ++calls.setup_unix; } + void setup_tcp(capy::any_stream&) { ++calls.setup_tcp; } + void setup_tcp_tls(capy::any_stream&) { ++calls.setup_tcp_tls; } + + capy::io_task<> unix_connect(const connect_params&) + { + ++calls.unix_connect; + co_return retval.unix_connect; + } + + capy::io_task tcp_resolve(const connect_params&) + { + ++calls.tcp_resolve; + std::vector entries{ + corosio::resolver_entry{make_endpoint("192.168.10.1:1234"), "my_host", "1234"}, + corosio::resolver_entry{make_endpoint("192.168.10.2:1235"), "my_host", "1234"}, + }; + co_return {retval.tcp_resolve, corosio::resolver_results{std::move(entries)}}; + } + + capy::io_task tcp_connect( + const connect_params&, + const corosio::resolver_results& results) + { + BOOST_TEST_NE(results.size(), 0u); + ++calls.tcp_connect; + co_return {retval.tcp_connect, *results.begin()}; + } + + capy::io_task<> tls_handshake(const connect_params&) + { + ++calls.tls_handshake; + co_return retval.tls_handshake; + } +}; + +// Reduce duplication +struct fixture : detail::log_fixture { + buffered_logger lgr{make_logger()}; + mock_impl impl; + capy::any_stream stream; +}; + +capy::task<> test_tcp_success() +{ + // Setup + fixture fix; + address addr{"some.host", "1234"}; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params({addr, false}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, std::error_code()); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_tcp = 1, + .tcp_resolve = 1, + .tcp_connect = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"}, + {logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" }, + // clang-format on + }); +} + +capy::task<> test_tcp_tls_success() +{ + // Setup + fixture fix; + address addr{"some.host", "1234"}; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params({addr, true}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, std::error_code()); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_tcp_tls = 1, + .tcp_resolve = 1, + .tcp_connect = 1, + .tls_handshake = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"}, + {logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" }, + {logger::level::debug, "Connect: SSL handshake succeeded" }, + // clang-format on + }); +} + +capy::task<> test_unix_success() +{ + // Setup + fixture fix; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params(any_address_view{"/tmp/redis.sock"}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, std::error_code()); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_unix = 1, + .unix_connect = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + {logger::level::debug, "Connect: UNIX socket connect succeeded"}, + }); +} + +// Resolve errors +capy::task<> test_tcp_resolve_error() +{ + // Setup + fixture fix; + fix.impl.retval.tcp_resolve = error::empty_field; + address addr{"some.host", "1234"}; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params({addr, false}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, error_code(error::empty_field)); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_tcp = 1, + .tcp_resolve = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::info, "Connect: hostname resolution failed: Expected field value is empty. [boost.redis:5]"}, + // clang-format on + }); +} + +// Connect errors +capy::task<> test_tcp_connect_error() +{ + // Setup + fixture fix; + fix.impl.retval.tcp_connect = error::empty_field; + address addr{"some.host", "1234"}; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params({addr, false}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, error_code(error::empty_field)); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_tcp = 1, + .tcp_resolve = 1, + .tcp_connect = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"}, + {logger::level::info, "Connect: TCP connect failed: Expected field value is empty. [boost.redis:5]"}, + // clang-format on + }); +} + +// SSL handshake error +capy::task<> test_ssl_handshake_error() +{ + // Setup + fixture fix; + fix.impl.retval.tls_handshake = error::empty_field; + address addr{"some.host", "1234"}; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params({addr, true}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, error_code(error::empty_field)); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_tcp_tls = 1, + .tcp_resolve = 1, + .tcp_connect = 1, + .tls_handshake = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"}, + {logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" }, + {logger::level::info, "Connect: SSL handshake failed: Expected field value is empty. [boost.redis:5]"}, + // clang-format on + }); +} + +// UNIX connect errors +capy::task<> test_unix_connect_error() +{ + // Setup + fixture fix; + fix.impl.retval.unix_connect = error::empty_field; + + // Call the function + auto [ec] = co_await co_connect( + fix.impl, + make_connect_params(any_address_view{"/tmp/redis.sock"}), + fix.lgr, + fix.stream); + BOOST_TEST_EQ(ec, error_code(error::empty_field)); + + // Mock expectations + constexpr num_calls expected_calls{ + .setup_unix = 1, + .unix_connect = 1, + }; + BOOST_TEST_EQ(fix.impl.calls, expected_calls); + + // Log + fix.check_log({ + // clang-format off + {logger::level::info, "Connect: UNIX socket connect failed: Expected field value is empty. [boost.redis:5]"}, + // clang-format on + }); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_tcp_success()); + run_coroutine_test(test_tcp_tls_success()); + run_coroutine_test(test_unix_success()); + + run_coroutine_test(test_tcp_resolve_error()); + run_coroutine_test(test_tcp_connect_error()); + run_coroutine_test(test_ssl_handshake_error()); + run_coroutine_test(test_unix_connect_error()); + + return boost::report_errors(); +} 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..9f5ff08b3 --- /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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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, capy_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..49f62d61b --- /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(make_test_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_compose_setup_request.cpp b/test/test_compose_setup_request.cpp index 566799502..fd1bb8f60 100644 --- a/test/test_compose_setup_request.cpp +++ b/test/test_compose_setup_request.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include @@ -21,7 +20,6 @@ #include using namespace boost::redis; -namespace asio = boost::asio; using detail::compose_setup_request; using detail::subscription_tracker; using boost::system::error_code; 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 06e79cdd8..50c7be00d 100644 --- a/test/test_conn_echo_stress.cpp +++ b/test/test_conn_echo_stress.cpp @@ -132,7 +132,7 @@ int main() 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_TEST_EQ(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 b7712cae8..e50a521fb 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -11,7 +11,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include #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 ae4719e66..b064568b3 100644 --- a/test/test_conn_exec_error.cpp +++ b/test/test_conn_exec_error.cpp @@ -8,7 +8,7 @@ #include -#include "common.hpp" +#include "asio_common.hpp" #include #include @@ -308,7 +308,7 @@ void test_issue_287_generic_response_error_then_success() bool run_finished = false, exec_finished = false; conn.async_run(cfg, [&](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_retry.cpp b/test/test_conn_exec_retry.cpp index 4c86a61ac..47209e1f2 100644 --- a/test/test_conn_exec_retry.cpp +++ b/test/test_conn_exec_retry.cpp @@ -67,13 +67,13 @@ void test_request_cancel_if_unresponded_true() auto c2 = [&](error_code ec, std::size_t) { c2_called = true; std::cout << "c2" << std::endl; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; auto c1 = [&](error_code ec, std::size_t) { c1_called = true; std::cout << "c1" << std::endl; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; auto c0 = [&](error_code ec, std::size_t) { @@ -147,7 +147,7 @@ void test_request_cancel_if_unresponded_false() auto c1 = [&](error_code ec, std::size_t) { c1_called = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(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 d4cf2a0ba..7d065742e 100644 --- a/test/test_conn_quit.cpp +++ b/test/test_conn_quit.cpp @@ -9,7 +9,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include #include @@ -49,7 +49,7 @@ void test_async_run_exits() auto c3 = [&](error_code ec, std::size_t) { c3_called = true; std::clog << "c3: " << ec.message() << std::endl; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(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 f91ca2d9a..45f55a800 100644 --- a/test/test_conn_reconnect.cpp +++ b/test/test_conn_reconnect.cpp @@ -23,6 +23,7 @@ int main() { } #include #include +#include "asio_common.hpp" #include "common.hpp" #include @@ -89,7 +90,7 @@ auto test_after_timeout() -> net::awaitable req1.push("BLPOP", "any", 0); co_await conn->async_exec(req1, ignore, net::cancel_after(1s, net::redirect_error(ec1))); - BOOST_TEST_EQ(ec1, net::error::operation_aborted); + BOOST_TEST_EQ(ec1, canceled_condition()); request req2; req2.get_config().cancel_if_not_connected = false; @@ -102,7 +103,7 @@ auto test_after_timeout() -> net::awaitable std::cout << "ccc" << std::endl; - BOOST_TEST_EQ(ec1, net::error::operation_aborted); + BOOST_TEST_EQ(ec1, canceled_condition()); } } // namespace 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 4359f209c..70e4bd345 100644 --- a/test/test_conn_tls.cpp +++ b/test/test_conn_tls.cpp @@ -81,7 +81,7 @@ void test_exec_default_ssl_context() 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); @@ -124,7 +124,7 @@ void test_exec_custom_ssl_context() 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); @@ -156,7 +156,7 @@ void test_reconnection() // 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()); }); // The PING is the end of the callback chain diff --git a/test/test_conversions.cpp b/test/test_conversions.cpp index 1fb3a5db5..c52275085 100644 --- a/test/test_conversions.cpp +++ b/test/test_conversions.cpp @@ -10,7 +10,7 @@ #include #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..73341d352 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -6,15 +6,16 @@ // 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" @@ -25,7 +26,8 @@ #include 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 +35,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 +142,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 +164,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 +181,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 +205,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 +223,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 +243,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 +265,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 +281,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 +301,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 +321,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 +334,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 +348,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 +373,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 +386,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 +398,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 +416,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 +431,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 +457,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 +471,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 93c90baef..a8015d134 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,14 +26,14 @@ #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 detail::multiplexer; 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; @@ -113,17 +113,17 @@ void test_success() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, 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(mpx.get_read_buffer(), payload); - act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -148,11 +148,11 @@ void test_no_expected_response() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM shouldn't ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // No adapter calls should be done @@ -168,25 +168,25 @@ void test_short_reads() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, 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(mpx.get_read_buffer(), payload.substr(0, 6u)); - act = fsm.resume(mpx, error_code(), 6u, cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), 6u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(mpx.get_read_buffer(), payload.substr(6, 10u)); - act = fsm.resume(mpx, error_code(), 10u, cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), 10u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(mpx.get_read_buffer(), payload.substr(16)); - act = fsm.resume(mpx, error_code(), payload.substr(16).size(), cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), payload.substr(16).size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -211,12 +211,12 @@ void test_write_error() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // Write error - act = fsm.resume(mpx, asio::error::connection_reset, 10u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::connection_reset)); + act = fsm.resume(mpx, make_error_code(errc::io_error), 10u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_write_cancel() @@ -227,12 +227,12 @@ void test_write_cancel() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, 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(mpx, error_code(), 10u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(mpx, error_code(), 10u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Errors in read @@ -244,16 +244,16 @@ void test_read_error() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read error - act = fsm.resume(mpx, asio::error::network_reset, 0u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::network_reset)); + act = fsm.resume(mpx, make_error_code(errc::io_error), 0u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_read_cancelled() @@ -264,17 +264,17 @@ void test_read_cancelled() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, 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(mpx.get_read_buffer(), "$5\r\n"); - act = fsm.resume(mpx, error_code(), 4u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(mpx, error_code(), 4u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Buffer too small @@ -287,11 +287,11 @@ void test_buffer_prepare_error() mpx.get_read_buffer().set_config({8u}); // max size is 8 bytes // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, 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(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size)); } @@ -304,17 +304,17 @@ void test_parse_error() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, 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(mpx.get_read_buffer(), payload); - act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::not_a_number)); } @@ -330,17 +330,17 @@ void test_adapter_error() multiplexer mpx; // Write the request - auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(mpx, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(mpx, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(mpx, 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(mpx.get_read_buffer(), payload); - act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(mpx, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); } diff --git a/test/test_flow_controller.cpp b/test/test_flow_controller.cpp new file mode 100644 index 000000000..2519da60f --- /dev/null +++ b/test/test_flow_controller.cpp @@ -0,0 +1,399 @@ +// +// 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 "corosio_common.hpp" + +#include +#include + +using namespace boost::redis; +using namespace boost::redis::test; +namespace capy = boost::capy; +using detail::flow_controller; + +namespace { + +// Yields twice so any pending handlers are dispatched. +// This is a better alternative than waiting for small periods of time with delay() +capy::task<> yield() +{ + capy::continuation cont{}; + + struct yield_awaitable { + capy::continuation* cont; + + constexpr bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + cont->h = h; + env->executor.post(*cont); + return std::noop_coroutine(); + } + + void await_resume() { } + } aw{&cont}; + + co_await aw; + co_await aw; +} + +// If take() is called in the initial state, it waits (regression check) +capy::task<> test_take_initial() +{ + flow_controller cont{64u}; + bool point1_reached = false; + + BOOST_TEST_EQ(cont.pending_bytes(), 0u); // initially, the object is empty + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // Initially, there are no bytes to be taken, so this blocks + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); // the object was emptied + + // Signal that take has returned + point1_reached = true; + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that take() didn't complete immediately + co_await yield(); + BOOST_TEST_NOT(point1_reached); + + // Unblock take() + BOOST_TEST(cont.try_put(20u)); + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// If take() is called and there are pending bytes, they get consumed. +capy::task<> test_take_pending_bytes() +{ + flow_controller cont{64u}; + bool point1_reached = false, point2_reached = false; + + // Place some bytes in the object + BOOST_TEST(cont.try_put(20u)); + BOOST_TEST_EQ(cont.pending_bytes(), 20u); + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // There are pending bytes, so this does not block + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + point1_reached = true; + + // Subsequent calls would block + auto [ec2] = co_await cont.take(); + BOOST_TEST_EQ(ec2, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + point2_reached = true; + + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that take() completed immediately and the 2nd one didn't + co_await yield(); + BOOST_TEST(point1_reached); + + co_await yield(); + BOOST_TEST_NOT(point2_reached); + + // Unblock take() + BOOST_TEST(cont.try_put(20)); + + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// take() can be cancelled +capy::task<> test_take_cancel() +{ + flow_controller cont{64u}; + + auto result = co_await capy::when_any( + [&]() -> capy::io_task<> { + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, capy_canceled_condition()); + co_return {}; + }(), + capy::ready()); + + BOOST_TEST_EQ(result.index(), 2u); // ready finished 1st +} + +// try_put() returns true if the object is not full +void test_try_put_not_full() +{ + flow_controller cont{64u}; + BOOST_TEST(cont.try_put(20u)); + BOOST_TEST_EQ(cont.pending_bytes(), 20u); + BOOST_TEST(cont.try_put(41u)); + BOOST_TEST_EQ(cont.pending_bytes(), 61u); + BOOST_TEST(cont.try_put(3u)); + BOOST_TEST_EQ(cont.pending_bytes(), 64u); +} + +// try_put() returns false if the object is just full +void test_try_put_just_full() +{ + flow_controller cont{64u}; + BOOST_TEST(cont.try_put(64u)); + BOOST_TEST_EQ(cont.pending_bytes(), 64u); + BOOST_TEST_NOT(cont.try_put(1u)); + BOOST_TEST_EQ(cont.pending_bytes(), 64u); +} + +// try_put() returns true if we fill past the max pending bytes, but the object is then considered full +void test_try_put_past_full() +{ + flow_controller cont{64u}; + BOOST_TEST(cont.try_put(100u)); + BOOST_TEST_EQ(cont.pending_bytes(), 100u); + BOOST_TEST_NOT(cont.try_put(1u)); + BOOST_TEST_EQ(cont.pending_bytes(), 100u); +} + +// try_put() (and in consequence, put()) obey the max_bytes constructor arg +void test_try_put_ctor_arg() +{ + flow_controller cont{100u}; + BOOST_TEST(cont.try_put(80u)); + BOOST_TEST(cont.try_put(19u)); + BOOST_TEST(cont.try_put(2u)); + BOOST_TEST_NOT(cont.try_put(1u)); + BOOST_TEST_EQ(cont.pending_bytes(), 101u); +} + +// put() completes immediately in the initial state (no pending bytes) +capy::task<> test_put_initial() +{ + flow_controller cont{64u}; + bool point1_reached = false; + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // There are no pending bytes, so this does not block + auto [ec] = co_await cont.put(21u); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 21u); + point1_reached = true; + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that put() completed immediately + co_await yield(); + BOOST_TEST(point1_reached); + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// put() completes immediately if the object is not full +capy::task<> test_put_not_full() +{ + flow_controller cont{64u}; + bool point1_reached = false; + + BOOST_TEST(cont.try_put(21u)); + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // There are no pending bytes, so this does not block + auto [ec] = co_await cont.put(18u); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 39u); + point1_reached = true; + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that put() completed immediately + co_await yield(); + BOOST_TEST(point1_reached); + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// put() blocks until a take() happens if the object is full +capy::task<> test_put_full() +{ + flow_controller cont{64u}; + bool point1_reached = false; + + BOOST_TEST(cont.try_put(80u)); // fill the object + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // The object is full, so this blocks + auto [ec] = co_await cont.put(18u); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 18u); + point1_reached = true; + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that put() blocked + co_await yield(); + BOOST_TEST_NOT(point1_reached); + + // Unblock put + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, std::error_code()); + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// put() unblocks take() +capy::task<> test_put_unblocks_take() +{ + flow_controller cont{64u}; + bool point1_reached = false; + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // The object is empty, so this blocks + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + point1_reached = true; + co_return {}; + }(), + [&]() -> capy::io_task<> { + // Verify that take() blocked + co_await yield(); + BOOST_TEST_NOT(point1_reached); + + // Calling put() unblocks take() + auto [ec] = co_await cont.put(10u); + BOOST_TEST_EQ(ec, std::error_code()); + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +// put() can be cancelled +capy::task<> test_put_cancel() +{ + flow_controller cont{64u}; + + BOOST_TEST(cont.try_put(80u)); // fill the object + + auto result = co_await capy::when_any( + [&]() -> capy::io_task<> { + auto [ec] = co_await cont.put(10u); // will block until cancelled + BOOST_TEST_EQ(ec, capy_canceled_condition()); + co_return {}; + }(), + capy::ready()); + + BOOST_TEST_EQ(result.index(), 2u); // ready finished 1st +} + +// Full cycle +capy::task<> test_full_cycle() +{ + flow_controller cont{64u}; + int receiver_point = 0, reader_point = 0; + + auto [ec, a, b] = co_await capy::when_all( + [&]() -> capy::io_task<> { + // This is the receiver. Initially, no push has arrived and it blocks + auto [ec] = co_await cont.take(); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + receiver_point = 1; + + // The pushes have been consumed. Block for more + auto [ec2] = co_await cont.take(); + BOOST_TEST_EQ(ec2, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + + // Check that the reader actually blocked + co_await yield(); + BOOST_TEST_EQ(reader_point, 1); + + // The pushes have been consumed. Block for more + auto [ec3] = co_await cont.take(); + BOOST_TEST_EQ(ec3, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 0u); + + co_return {}; + }(), + [&]() -> capy::io_task<> { + // A push arrives. There is room, so we just try_put and don't block + BOOST_TEST(cont.try_put(23u)); + + // No more pushes arrive for now, allow the consumer to catch up + co_await yield(); + BOOST_TEST_EQ(receiver_point, 1); + + // A burst of pushes arrives. It's too much and the controller blocks + BOOST_TEST(cont.try_put(21u)); + BOOST_TEST(cont.try_put(8u)); + BOOST_TEST(cont.try_put(50u)); + BOOST_TEST_NOT(cont.try_put(10u)); + BOOST_TEST_EQ(cont.pending_bytes(), 79u); + reader_point = 1; + + // Because the last try_put returned false, we block now + auto [ec] = co_await cont.put(10u); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(cont.pending_bytes(), 10u); + + co_return {}; + }()); + + BOOST_TEST_EQ(ec, std::error_code()); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_take_initial()); + run_coroutine_test(test_take_pending_bytes()); + run_coroutine_test(test_take_cancel()); + + test_try_put_not_full(); + test_try_put_just_full(); + test_try_put_past_full(); + test_try_put_ctor_arg(); + + run_coroutine_test(test_put_initial()); + run_coroutine_test(test_put_not_full()); + run_coroutine_test(test_put_full()); + run_coroutine_test(test_put_unblocks_take()); + run_coroutine_test(test_put_cancel()); + + run_coroutine_test(test_full_cycle()); + + return boost::report_errors(); +} diff --git a/test/test_mix_asio_corosio.cpp b/test/test_mix_asio_corosio.cpp new file mode 100644 index 000000000..195b4c1a6 --- /dev/null +++ b/test/test_mix_asio_corosio.cpp @@ -0,0 +1,54 @@ +// +// 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) +// + +// The Asio and Corosio implementations can be mixed freely without ODR violations. +// Mixing all the src files is OK, too +#include +#include +#include + +// Regular includes +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace boost::redis; +namespace asio = boost::asio; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace std::chrono_literals; + +namespace { + +void test_corosio() +{ + corosio::io_context ctx; + co_connection conn{ctx}; +} + +void test_asio() +{ + asio::io_context ctx; + connection conn{ctx}; +} + +} // namespace + +int main() +{ + test_corosio(); + test_asio(); + + return boost::report_errors(); +} 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 507b2f4ab..b47ff50c4 100644 --- a/test/test_sentinel_resolve_fsm.cpp +++ b/test/test_sentinel_resolve_fsm.cpp @@ -7,13 +7,14 @@ // #include +#include #include #include #include -#include #include #include +#include #include #include @@ -24,12 +25,13 @@ 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; using detail::tree_from_resp3; using boost::system::error_code; -using boost::asio::cancellation_type_t; static char const* to_string(sentinel_action::type t) { @@ -110,11 +112,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 @@ -126,7 +128,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 @@ -161,11 +163,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 @@ -182,7 +184,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. @@ -214,15 +216,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", @@ -230,7 +232,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 @@ -256,21 +258,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 @@ -297,9 +299,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", @@ -307,15 +309,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 @@ -343,9 +345,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", @@ -353,15 +355,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 @@ -388,9 +390,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", @@ -399,15 +401,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 @@ -435,9 +437,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", @@ -446,9 +448,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 @@ -459,7 +461,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 @@ -486,9 +488,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", @@ -496,13 +498,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", @@ -510,7 +512,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 @@ -551,16 +553,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 @@ -585,12 +587,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({ @@ -606,12 +608,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({ @@ -627,12 +629,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({ @@ -649,12 +651,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..775380c0d 100644 --- a/test/test_writer_fsm.cpp +++ b/test/test_writer_fsm.cpp @@ -6,16 +6,18 @@ // 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 "sansio_utils.hpp" @@ -24,8 +26,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 +38,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 +113,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 +134,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 +147,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 +174,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 +183,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 +208,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, make_error_code(errc::protocol_error), 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 +272,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 +314,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 +352,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 +375,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 +413,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 +442,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 +471,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 +498,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 +523,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 +548,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(); diff --git a/tools/ci.py b/tools/ci.py index 26ea85926..d4f6501a2 100755 --- a/tools/ci.py +++ b/tools/ci.py @@ -133,6 +133,10 @@ def _setup_boost( _run(["git", "submodule", "update", "-q", "--init", "tools/boostdep"]) _run(["python3", "tools/boostdep/depinst/depinst.py", "--include", "example", "redis"]) + # Manually add capy and corosio. TODO: remove this once they get accepted into Boost + _run(["git", "clone", "-b", "develop", "--depth", "1", "https://github.com/cppalliance/capy.git", "libs/capy"]) + _run(["git", "clone", "-b", "develop", "--depth", "1", "https://github.com/cppalliance/corosio.git", "libs/corosio"]) + # Bootstrap if _is_windows: _run(['cmd', '/q', '/c', 'bootstrap.bat']) @@ -144,14 +148,17 @@ def _setup_boost( # Builds a Boost distribution using ./b2 install, and places it into _b2_distro. # This emulates a regular Boost distribution, like the ones in releases def _build_b2_distro( - toolset: str + toolset: str, + cxxstd: str ): os.chdir(str(_boost_root)) _run([ _b2_command, '--prefix={}'.format(_b2_distro), - '--with-headers', + '--with-capy', + '--with-corosio', 'toolset={}'.format(toolset), + 'cxxstd={}'.format(cxxstd), '-d0', 'install' ]) @@ -161,6 +168,8 @@ def _build_b2_distro( # It includes only our library and any dependency. # When integration_tests is True, tests requiring a live Redis server are also # built and run; otherwise only the unit tests are. +# When corosio_api is False, the Corosio API target and its tests/examples are +# skipped (used for compilers that don't support Corosio). def _build_cmake_distro( generator: str, build_type: str, @@ -168,6 +177,7 @@ def _build_cmake_distro( toolset: str, build_shared_libs: bool = False, integration_tests: bool = False, + corosio_api: bool = True, cxxflags: str = '', ldflags: str = '' ): @@ -185,6 +195,7 @@ def _build_cmake_distro( '-DBUILD_SHARED_LIBS={}'.format(_cmake_bool(build_shared_libs)), '-DCMAKE_INSTALL_PREFIX={}'.format(_cmake_distro), '-DBOOST_REDIS_INTEGRATION_TESTS={}'.format(_cmake_bool(integration_tests)), + '-DBOOST_REDIS_COROSIO_API={}'.format(_cmake_bool(corosio_api)), '-DBoost_VERBOSE=ON', '-DCMAKE_INSTALL_MESSAGE=NEVER', '..' @@ -194,7 +205,8 @@ def _build_cmake_distro( _run(['cmake', '--build', '.', '--target', 'install', '--config', build_type]) -# Tests that the library can be consumed using add_subdirectory() +# Tests that the library can be consumed using add_subdirectory(). +# When corosio_api is True, the Corosio variant of the test is also exercised. def _run_cmake_add_subdirectory_tests( generator: str, build_type: str, @@ -202,7 +214,8 @@ def _run_cmake_add_subdirectory_tests( toolset: str, build_shared_libs: bool = False, cxxflags: str = '', - ldflags: str = '' + ldflags: str = '', + corosio_api: bool = True ): _set_cmake_env(cxxflags, ldflags) test_folder = _boost_root.joinpath('libs', 'redis', 'test', 'cmake_subdir_test', '__build') @@ -216,19 +229,22 @@ def _run_cmake_add_subdirectory_tests( '-DCMAKE_BUILD_TYPE={}'.format(build_type), '-DBUILD_SHARED_LIBS={}'.format(_cmake_bool(build_shared_libs)), '-DCMAKE_CXX_STANDARD={}'.format(cxxstd), + '-DBOOST_REDIS_COROSIO_API={}'.format(_cmake_bool(corosio_api)), '..' ]) _run(['cmake', '--build', '.', '--config', build_type]) _run(['ctest', '--output-on-failure', '--build-config', build_type, '--no-tests=error']) -# Tests that the library can be consumed using find_package on a distro built by cmake +# Tests that the library can be consumed using find_package on a distro built by cmake. +# When corosio_api is True, the Corosio variant of the test is also exercised. def _run_cmake_find_package_tests( generator: str, build_type: str, cxxstd: str, toolset: str, build_shared_libs: bool = False, + corosio_api: bool = True, cxxflags: str = '', ldflags: str = '' ): @@ -243,6 +259,7 @@ def _run_cmake_find_package_tests( '-DCMAKE_BUILD_TYPE={}'.format(build_type), '-DBUILD_SHARED_LIBS={}'.format(_cmake_bool(build_shared_libs)), '-DCMAKE_CXX_STANDARD={}'.format(cxxstd), + '-DBOOST_REDIS_COROSIO_API={}'.format(_cmake_bool(corosio_api)), '-DCMAKE_PREFIX_PATH={}'.format(_cmake_distro), '..' ]) @@ -250,13 +267,15 @@ def _run_cmake_find_package_tests( _run(['ctest', '--output-on-failure', '--build-config', build_type, '--no-tests=error']) -# Tests that the library can be consumed using find_package on a distro built by b2 +# Tests that the library can be consumed using find_package on a distro built by b2. +# When corosio_api is True, the Corosio variant of the test is also exercised. def _run_cmake_b2_find_package_tests( generator: str, build_type: str, cxxstd: str, toolset: str, build_shared_libs: bool = False, + corosio_api: bool = True, cxxflags: str = '', ldflags: str = '' ): @@ -285,6 +304,9 @@ def _run_b2_tests( cxxstd: str, toolset: str ): + # TODO: recover this after https://github.com/cppalliance/corosio/issues/245 + werror = 'off' if _is_windows else 'off' + os.chdir(str(_boost_root)) _run([ _b2_command, @@ -293,7 +315,7 @@ def _run_b2_tests( 'cxxstd={}'.format(cxxstd), 'variant={}'.format(variant), 'warnings=extra', - 'warnings-as-errors=on', + 'warnings-as-errors={}'.format(werror), '-j4', 'libs/redis/test', 'libs/redis/test//fail_if_no_openssl' @@ -311,6 +333,7 @@ def main(): subp = subparsers.add_parser('build-b2-distro') subp.add_argument('--toolset', default='gcc') + subp.add_argument('--cxxstd', default='20') subp.set_defaults(func=_build_b2_distro) subp = subparsers.add_parser('build-cmake-distro') @@ -320,6 +343,7 @@ def main(): subp.add_argument('--toolset', default='gcc') subp.add_argument('--build-shared-libs', type=_str2bool, default=False) subp.add_argument('--integration-tests', type=_str2bool, default=True) + subp.add_argument('--corosio-api', type=_str2bool, default=True) subp.add_argument('--cxxflags', default='') subp.add_argument('--ldflags', default='') subp.set_defaults(func=_build_cmake_distro) @@ -330,6 +354,7 @@ def main(): subp.add_argument('--cxxstd', default='20') subp.add_argument('--toolset', default='gcc') subp.add_argument('--build-shared-libs', type=_str2bool, default=False) + subp.add_argument('--corosio-api', type=_str2bool, default=True) subp.add_argument('--cxxflags', default='') subp.add_argument('--ldflags', default='') subp.set_defaults(func=_run_cmake_add_subdirectory_tests) @@ -340,6 +365,7 @@ def main(): subp.add_argument('--cxxstd', default='20') subp.add_argument('--toolset', default='gcc') subp.add_argument('--build-shared-libs', type=_str2bool, default=False) + subp.add_argument('--corosio-api', type=_str2bool, default=True) subp.add_argument('--cxxflags', default='') subp.add_argument('--ldflags', default='') subp.set_defaults(func=_run_cmake_find_package_tests) @@ -350,6 +376,7 @@ def main(): subp.add_argument('--cxxstd', default='20') subp.add_argument('--toolset', default='gcc') subp.add_argument('--build-shared-libs', type=_str2bool, default=False) + subp.add_argument('--corosio-api', type=_str2bool, default=True) subp.add_argument('--cxxflags', default='') subp.add_argument('--ldflags', default='') subp.set_defaults(func=_run_cmake_b2_find_package_tests)