2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Boost 1.90.0 beta1: merge develop to master

This commit is contained in:
Anarthal (Rubén Pérez)
2025-10-27 11:03:05 +01:00
committed by GitHub
96 changed files with 7818 additions and 2780 deletions

View File

@@ -137,6 +137,7 @@ jobs:
cxxstd: '17'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: gcc-11
install: g++-11
@@ -144,6 +145,7 @@ jobs:
cxxstd: '20'
build-type: 'Release'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-11
install: clang-11
@@ -151,6 +153,7 @@ jobs:
cxxstd: '17'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-11
install: clang-11
@@ -158,6 +161,7 @@ jobs:
cxxstd: '20'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-13
install: clang-13
@@ -165,6 +169,7 @@ jobs:
cxxstd: '17'
build-type: 'Release'
ldflags: ''
server: "redis:8.2.1-alpine"
- toolset: clang-13
install: clang-13
@@ -172,6 +177,7 @@ jobs:
cxxstd: '20'
build-type: 'Release'
ldflags: ''
server: "redis:8.2.1-alpine"
- toolset: clang-14
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
@@ -180,6 +186,7 @@ jobs:
build-type: 'Debug'
cxxflags: '-stdlib=libc++'
ldflags: '-lc++'
server: "redis:8.2.1-alpine"
- toolset: clang-14
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
@@ -188,6 +195,7 @@ jobs:
build-type: 'Release'
cxxflags: '-stdlib=libc++'
ldflags: '-lc++'
server: "redis:8.2.1-alpine"
- toolset: clang-19
install: 'clang-19'
@@ -196,6 +204,7 @@ jobs:
build-type: 'Debug'
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
ldflags: '-fsanitize=address -fsanitize=undefined'
server: "redis:8.2.1-alpine"
- toolset: gcc-14
install: 'g++-14'
@@ -203,6 +212,7 @@ jobs:
cxxstd: '23'
build-type: 'Debug'
cxxflags: '-DBOOST_ASIO_DISABLE_LOCAL_SOCKETS=1' # If a system had no UNIX socket support, we build correctly
server: "valkey/valkey:8.1.3-alpine"
- toolset: gcc-14
install: 'g++-14'
@@ -211,6 +221,7 @@ jobs:
build-type: 'Debug'
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
ldflags: '-fsanitize=address -fsanitize=undefined'
server: "valkey/valkey:8.1.3-alpine"
runs-on: ubuntu-latest
env:
@@ -224,7 +235,7 @@ jobs:
- name: Set up the required containers
run: |
IMAGE=${{ matrix.container }} docker compose -f tools/docker-compose.yml up -d --wait || (docker compose logs; exit 1)
BUILDER_IMAGE=${{ matrix.container }} SERVER_IMAGE=${{ matrix.server }} docker compose -f tools/docker-compose.yml up -d --wait || (docker compose logs; exit 1)
- name: Install dependencies
run: |

View File

@@ -1,178 +0,0 @@
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 14,
"patch": 0
},
"configurePresets": [
{
"name": "cmake-pedantic",
"hidden": true,
"warnings": {
"dev": true,
"deprecated": true,
"uninitialized": false,
"unusedCli": true,
"systemVars": false
},
"errors": {
"dev": true,
"deprecated": true
}
},
{
"name": "coverage",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/coverage",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Coverage",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra",
"CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage",
"CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage",
"PROJECT_BINARY_DIR": "${sourceDir}/build/coverage",
"COVERAGE_HTML_COMMAND": ""
}
},
{
"name": "g++-11",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/g++-11",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11"
}
},
{
"name": "g++-11-release",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/g++-11-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra",
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-release"
}
},
{
"name": "clang++-13",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/clang++-13",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "clang++-13",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-13"
}
},
{
"name": "clang++-14",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/clang++-14",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-14"
}
},
{
"name": "libc++-14-cpp17",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp17",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp17"
}
},
{
"name": "libc++-14-cpp20",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp20",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp20"
}
},
{
"name": "clang-tidy",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["g++-11"],
"binaryDir": "${sourceDir}/build/clang-tidy",
"cacheVariables": {
"CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/include/*",
"CMAKE_CXX_STANDARD": "20"
}
}
],
"buildPresets": [
{ "name": "coverage", "configurePreset": "coverage" },
{ "name": "g++-11", "configurePreset": "g++-11" },
{ "name": "g++-11-release", "configurePreset": "g++-11-release" },
{ "name": "clang++-13", "configurePreset": "clang++-13" },
{ "name": "clang++-14", "configurePreset": "clang++-14" },
{ "name": "libc++-14-cpp17", "configurePreset": "libc++-14-cpp17" },
{ "name": "libc++-14-cpp20", "configurePreset": "libc++-14-cpp20" },
{ "name": "clang-tidy", "configurePreset": "clang-tidy" }
],
"testPresets": [
{
"name": "test",
"hidden": true,
"output": {"outputOnFailure": true},
"execution": {"noTestsAction": "error", "stopOnFailure": true}
},
{ "name": "coverage", "configurePreset": "coverage", "inherits": ["test"] },
{ "name": "g++-11", "configurePreset": "g++-11", "inherits": ["test"] },
{ "name": "g++-11-release", "configurePreset": "g++-11-release", "inherits": ["test"] },
{ "name": "clang++-13", "configurePreset": "clang++-13", "inherits": ["test"] },
{ "name": "clang++-14", "configurePreset": "clang++-14", "inherits": ["test"] },
{ "name": "libc++-14-cpp17", "configurePreset": "libc++-14-cpp17", "inherits": ["test"] },
{ "name": "libc++-14-cpp20", "configurePreset": "libc++-14-cpp20", "inherits": ["test"] },
{ "name": "clang-tidy", "configurePreset": "clang-tidy", "inherits": ["test"] }
]
}

View File

@@ -16,4 +16,4 @@ cd "$SCRIPT_DIR"
export BOOST_SRC_DIR=$(realpath $SCRIPT_DIR/../../..)
npm ci
npx antora --log-format=pretty redis-playbook.yml
npx antora --log-format=pretty --stacktrace --log-level info redis-playbook.yml

View File

@@ -1,5 +1,6 @@
* xref:index.adoc[Introduction]
* xref:requests_responses.adoc[]
* xref:cancellation.adoc[]
* xref:serialization.adoc[]
* xref:logging.adoc[]
* xref:benchmarks.adoc[]

View File

@@ -0,0 +1,79 @@
//
// 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)
//
= 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`.
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
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;
// 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 an exception will be thrown.
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
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.
== Retrying idempotent requests
We mentioned that `async_exec` 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.
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_.
The `SET` command is idempotent, while `INCR` is not.
If you know that a `request` object contains only idempotent commands,
you can instruct Boost.Redis to retry the request on failure, even
if the library is unsure about the server having processed the request or not.
You can do so by setting `cancel_if_unresponded`
in xref:reference:boost/redis/request/config.adoc[`request::config`]
to false:
[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);
----

View File

@@ -8,7 +8,8 @@
[#intro]
= Introduction
Boost.Redis is a high-level https://redis.io/[Redis] client library built on top of
Boost.Redis is a high-level https://redis.io/[Redis] and https://valkey.io/[Valkey]
client library built on top of
https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio]
that implements the Redis protocol
https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md[RESP3].
@@ -19,7 +20,8 @@ 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.
* Redis 6 or higher (must support RESP3).
* 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].
@@ -137,6 +139,7 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
Here is a list of topics that you might be interested in:
* xref:cancellation.adoc[Setting timeouts to requests and managing cancellation].
* xref:requests_responses.adoc[More on requests and responses].
* xref:serialization.adoc[Serializing and parsing into custom types].
* xref:logging.adoc[Configuring logging].

View File

@@ -37,7 +37,7 @@ 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`]
as already stated. Requests accept a xref:reference:boost/redis/request/config[`boost::redis::request::config`]
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.

View File

@@ -17,9 +17,7 @@ endmacro()
macro(make_testable_example EXAMPLE_NAME STANDARD)
make_example(${EXAMPLE_NAME} ${STANDARD})
if (BOOST_REDIS_INTEGRATION_TESTS)
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME})
endif()
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379)
endmacro()
make_testable_example(cpp17_intro 17)
@@ -28,13 +26,14 @@ make_testable_example(cpp17_intro_sync 17)
make_testable_example(cpp20_intro 20)
make_testable_example(cpp20_containers 20)
make_testable_example(cpp20_json 20)
make_testable_example(cpp20_intro_tls 20)
make_testable_example(cpp20_unix_sockets 20)
make_testable_example(cpp20_timeouts 20)
make_example(cpp20_subscriber 20)
make_example(cpp20_streams 20)
make_example(cpp20_echo_server 20)
make_example(cpp20_resolve_with_sentinel 20)
make_example(cpp20_intro_tls 20)
# We test the protobuf example only on gcc.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")

View File

@@ -47,13 +47,8 @@ static void do_log(redis::logger::level level, std::string_view msg)
spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg);
}
auto main(int argc, char* argv[]) -> int
auto main(int argc, char** argv) -> int
{
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <server-host> <server-port>\n";
exit(1);
}
try {
// Create an execution context, required to create any I/O objects
asio::io_context ioc;
@@ -67,10 +62,12 @@ auto main(int argc, char* argv[]) -> int
redis::logger{redis::logger::level::info, do_log}
};
// Configuration to connect to the server
// Configuration to connect to the server. Adjust as required
redis::config cfg;
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
// Run the connection with the specified configuration.
// This will establish the connection and keep it healthy

View File

@@ -7,8 +7,10 @@
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/signal_set.hpp>

View File

@@ -7,6 +7,7 @@
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>

View File

@@ -7,7 +7,9 @@
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/signal_set.hpp>

View File

@@ -6,6 +6,7 @@
#include <boost/redis/connection.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/use_awaitable.hpp>

View File

@@ -0,0 +1,49 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::connection;
using namespace std::chrono_literals;
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> asio::awaitable<void>
{
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, asio::consign(asio::detached, conn));
// A request containing only a ping command.
request req;
req.push("PING", "Hello world");
// Response where the PONG response will be stored.
response<std::string> resp;
// Executes the request with a timeout. If the server is down,
// async_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 cancel_after.
// If the request hasn't completed after 10 seconds, an exception will be thrown.
co_await conn->async_exec(req, resp, asio::cancel_after(10s));
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -34,24 +34,41 @@ namespace boost::redis {
*/
class any_adapter {
public:
using fn_type = std::function<void(std::size_t, resp3::node_view const&, system::error_code&)>;
/** @brief Parse events that an adapter must support.
*/
enum class parse_event
{
/// Called before the parser starts processing data
init,
/// Called for each and every node of RESP3 data
node,
/// Called when done processing a complete RESP3 message
done
};
struct impl_t {
fn_type adapt_fn;
std::size_t supported_response_size;
} impl_;
/// The type erased implementation type.
using impl_t = std::function<void(parse_event, resp3::node_view const&, system::error_code&)>;
template <class T>
static auto create_impl(T& resp) -> impl_t
{
using namespace boost::redis::adapter;
auto adapter = boost_redis_adapt(resp);
std::size_t size = adapter.get_supported_response_size();
return {std::move(adapter), size};
return [adapter2 = boost_redis_adapt(resp)](
any_adapter::parse_event ev,
resp3::node_view const& nd,
system::error_code& ec) mutable {
switch (ev) {
case parse_event::init: adapter2.on_init(); break;
case parse_event::node: adapter2.on_node(nd, ec); break;
case parse_event::done: adapter2.on_done(); break;
}
};
}
template <class Executor>
friend class basic_connection;
/// Contructs from a type erased adaper
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
: impl_{std::move(fn)}
{ }
/**
* @brief Constructor.
@@ -67,6 +84,29 @@ public:
explicit any_adapter(T& resp)
: impl_(create_impl(resp))
{ }
/// Calls the implementation with the arguments `impl_(parse_event::init, ...);`
void on_init()
{
system::error_code ec;
impl_(parse_event::init, {}, ec);
};
/// Calls the implementation with the arguments `impl_(parse_event::done, ...);`
void on_done()
{
system::error_code ec;
impl_(parse_event::done, {}, ec);
};
/// Calls the implementation with the arguments `impl_(parse_event::node, ...);`
void on_node(resp3::node_view const& nd, system::error_code& ec)
{
impl_(parse_event::node, nd, ec);
};
private:
impl_t impl_;
};
} // namespace boost::redis

View File

@@ -147,8 +147,12 @@ public:
explicit general_aggregate(Result* c = nullptr)
: result_(c)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
@@ -160,12 +164,14 @@ public:
};
break;
default:
result_->value().push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
});
if (result_->has_value()) {
(**result_).push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
});
}
}
}
};
@@ -180,8 +186,11 @@ public:
: result_(t)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
@@ -206,8 +215,11 @@ class simple_impl {
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
{
if (is_aggregate(node.data_type)) {
ec = redis::error::expects_resp3_simple_type;
@@ -226,8 +238,11 @@ private:
public:
void on_value_available(Result& result) { hint_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (nd.data_type != resp3::type::set)
@@ -257,8 +272,11 @@ private:
public:
void on_value_available(Result& result) { current_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (element_multiplicity(nd.data_type) != 2)
@@ -292,8 +310,11 @@ class vector_impl {
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
auto const m = element_multiplicity(nd.data_type);
@@ -313,8 +334,11 @@ private:
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (i_ != -1) {
@@ -344,8 +368,11 @@ template <class Result>
struct list_impl {
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (!is_aggregate(nd.data_type)) {
BOOST_ASSERT(nd.aggregate_size == 1);
@@ -468,8 +495,11 @@ public:
}
}
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
@@ -480,7 +510,7 @@ public:
return;
BOOST_ASSERT(result_);
impl_(result_->value(), nd, ec);
impl_.on_node(result_->value(), nd, ec);
}
};
@@ -514,8 +544,11 @@ public:
: result_(o)
{ }
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
@@ -533,7 +566,7 @@ public:
impl_.on_value_available(result_->value().value());
}
impl_(result_->value().value(), nd, ec);
impl_.on_node(result_->value().value(), nd, ec);
}
};

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -8,6 +8,7 @@
#define BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
#include <boost/redis/adapter/detail/result_traits.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/response.hpp>
@@ -21,26 +22,6 @@
namespace boost::redis::adapter::detail {
class ignore_adapter {
public:
template <class String>
void operator()(std::size_t, resp3::basic_node<String> const& nd, system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
case resp3::type::null: ec = redis::error::resp3_null; break;
default: ;
}
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{
return static_cast<std::size_t>(-1);
}
};
template <class Response>
class static_adapter {
private:
@@ -50,51 +31,44 @@ private:
using adapters_array_type = std::array<variant_type, size>;
adapters_array_type adapters_;
std::size_t i_ = 0;
public:
explicit static_adapter(Response& r) { assigner<size - 1>::assign(adapters_, r); }
[[nodiscard]]
auto get_supported_response_size() const noexcept
{
return size;
}
template <class String>
void operator()(std::size_t i, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_init()
{
using std::visit;
// I am usure whether this should be an error or an assertion.
BOOST_ASSERT(i < adapters_.size());
visit(
[&](auto& arg) {
arg(nd, ec);
arg.on_init();
},
adapters_.at(i));
adapters_.at(i_));
}
};
template <class Vector>
class vector_adapter {
private:
using adapter_type = typename result_traits<Vector>::adapter_type;
adapter_type adapter_;
public:
explicit vector_adapter(Vector& v)
: adapter_{internal_adapt(v)}
{ }
[[nodiscard]]
auto get_supported_response_size() const noexcept
void on_done()
{
return static_cast<std::size_t>(-1);
using std::visit;
visit(
[&](auto& arg) {
arg.on_done();
},
adapters_.at(i_));
i_ += 1;
}
template <class String>
void operator()(std::size_t, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
adapter_(nd, ec);
using std::visit;
// I am usure whether this should be an error or an assertion.
BOOST_ASSERT(i_ < adapters_.size());
visit(
[&](auto& arg) {
arg.on_node(nd, ec);
},
adapters_.at(i_));
}
};
@@ -104,25 +78,25 @@ struct response_traits;
template <>
struct response_traits<ignore_t> {
using response_type = ignore_t;
using adapter_type = detail::ignore_adapter;
using adapter_type = ignore;
static auto adapt(response_type&) noexcept { return detail::ignore_adapter{}; }
static auto adapt(response_type&) noexcept { return ignore{}; }
};
template <>
struct response_traits<result<ignore_t>> {
using response_type = result<ignore_t>;
using adapter_type = detail::ignore_adapter;
using adapter_type = ignore;
static auto adapt(response_type&) noexcept { return detail::ignore_adapter{}; }
static auto adapt(response_type&) noexcept { return ignore{}; }
};
template <class String, class Allocator>
struct response_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = vector_adapter<response_type>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{v}; }
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class... Ts>
@@ -133,35 +107,6 @@ struct response_traits<response<Ts...>> {
static auto adapt(response_type& r) noexcept { return adapter_type{r}; }
};
template <class Adapter>
class wrapper {
public:
explicit wrapper(Adapter adapter)
: adapter_{adapter}
{ }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
{
return adapter_(0, nd, ec);
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{
return adapter_.get_supported_response_size();
}
private:
Adapter adapter_;
};
template <class Adapter>
auto make_adapter_wrapper(Adapter adapter)
{
return wrapper{adapter};
}
} // namespace boost::redis::adapter::detail
#endif // BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP

View File

@@ -132,8 +132,32 @@ public:
}
}
void on_init()
{
using std::visit;
for (auto& adapter : adapters_) {
visit(
[&](auto& arg) {
arg.on_init();
},
adapter);
}
}
void on_done()
{
using std::visit;
for (auto& adapter : adapters_) {
visit(
[&](auto& arg) {
arg.on_done();
},
adapter);
}
}
template <class String>
void operator()(resp3::basic_node<String> const& elem, system::error_code& ec)
void on_node(resp3::basic_node<String> const& elem, system::error_code& ec)
{
using std::visit;
@@ -148,9 +172,9 @@ public:
visit(
[&](auto& arg) {
arg(elem, ec);
arg.on_node(elem, ec);
},
adapters_[i_]);
adapters_.at(i_));
count(elem);
}
};

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -19,7 +19,10 @@ namespace boost::redis::adapter {
* RESP3 errors won't be ignored.
*/
struct ignore {
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code& ec)
void on_init() { }
void on_done() { }
void on_node(resp3::basic_node<std::string_view> const& nd, system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;

View File

@@ -8,6 +8,7 @@
#ifndef BOOST_REDIS_ADAPTER_RESULT_HPP
#define BOOST_REDIS_ADAPTER_RESULT_HPP
#include <boost/redis/detail/resp3_type_to_error.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
@@ -56,15 +57,9 @@ using result = system::result<Value, error>;
*/
BOOST_NORETURN inline void throw_exception_from_error(error const& e, boost::source_location const&)
{
system::error_code ec;
switch (e.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
case resp3::type::null: ec = redis::error::resp3_null; break;
default: BOOST_ASSERT_MSG(false, "Unexpected data type.");
}
throw system::system_error(ec, e.diagnostic);
throw system::system_error(
system::error_code(detail::resp3_type_to_error(e.data_type)),
e.diagnostic);
}
} // namespace boost::redis::adapter

View File

@@ -7,6 +7,8 @@
#ifndef BOOST_REDIS_CONFIG_HPP
#define BOOST_REDIS_CONFIG_HPP
#include <boost/redis/request.hpp>
#include <chrono>
#include <limits>
#include <optional>
@@ -40,24 +42,48 @@ struct config {
*/
std::string unix_socket;
/** @brief Username passed to the `HELLO` command.
* If left empty `HELLO` will be sent without authentication parameters.
/** @brief Username used for authentication during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* authentication is performed by sending a `HELLO` command.
* This field contains the username to employ.
*
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*/
std::string username = "default";
/** @brief Password passed to the
* `HELLO` command. If left
* empty `HELLO` will be sent without authentication parameters.
/** @brief Password used for authentication during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* authentication is performed by sending a `HELLO` command.
* This field contains the password to employ.
*
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*/
std::string password;
/// Client name parameter of the `HELLO` command.
/** @brief Client name parameter to use during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* a `HELLO` command is sent. If this field is not empty, the `HELLO` command
* will contain a `SETNAME` subcommand containing this value.
*/
std::string clientname = "Boost.Redis";
/// Database that will be passed to the `SELECT` command.
/** @brief Database index to pass to the `SELECT` command during connection establishment.
*
* If @ref use_setup is false (the default), and this field is set to a
* non-empty optional, and its value is different than zero,
* a `SELECT` command will be issued during connection establishment to set the logical
* database index. By default, no `SELECT` command is sent.
*/
std::optional<int> database_index = 0;
/// Message used by the health-checker in @ref boost::redis::basic_connection::async_run.
/// Message used by `PING` commands sent by the health checker.
std::string health_check_id = "Boost.Redis";
/**
@@ -79,7 +105,24 @@ struct config {
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
/** @brief Time span between successive health checks.
* Set to zero to disable health-checks pass zero as duration.
* Set to zero to disable health-checks.
*
* When this value is set to a non-zero duration, @ref basic_connection::async_run
* will issue `PING` commands whenever no command is sent to the server for more
* than `health_check_interval`. You can configure the message passed to the `PING`
* command using @ref health_check_id.
*
* Enabling health checks also sets timeouts to individual network
* operations. The connection is considered dead if:
*
* @li No byte can be written to the server after `health_check_interval`.
* @li No byte is read from the server after `2 * health_check_interval`.
*
* If the health checker finds that the connection is unresponsive, it will be closed,
* and a reconnection will be triggered, as if a network error had occurred.
*
* The exact timeout values are *not* part of the interface, and might change
* in future versions.
*/
std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2};
@@ -88,12 +131,54 @@ struct config {
*/
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
/** @brief Maximum size of a socket read, in bytes.
/** @brief Maximum size of the socket read-buffer in bytes.
*
* Sets a limit on how much data is allowed to be read into the
* read buffer. It can be used to prevent DDOS.
*/
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
/** @brief Grow size of the read buffer.
*
* The size by which the read buffer grows when more space is
* needed. This can help avoiding some memory allocations. Once the
* maximum size is reached no more memory allocations are made
* since the buffer is reused.
*/
std::size_t read_buffer_append_size = 4096;
/** @brief Enables using a custom requests during connection establishment.
*
* If set to true, the @ref setup member will be sent to the server immediately after
* connection establishment. Every time a reconnection happens, the setup
* request will be executed before any other request.
* It can be used to perform authentication,
* subscribe to channels or select a database index.
*
* When set to true, *the custom setup request replaces the built-in HELLO
* request generated by the library*. The @ref username, @ref password,
* @ref clientname and @ref database_index fields *will be ignored*.
*
* By default, @ref setup contains a `"HELLO 3"` command, which upgrades the
* protocol to RESP3. You might modify this request as you like,
* but you should ensure that the resulting connection uses RESP3.
*
* To prevent sending any setup request at all, set this field to true
* and @ref setup to an empty request. This can be used to interface with
* systems that don't support `HELLO`.
*
* By default, this field is false, and @ref setup will not be used.
*/
bool use_setup = false;
/** @brief Request to be executed after connection establishment.
*
* This member is only used if @ref use_setup is `true`. Please consult
* @ref use_setup docs for more info.
*
* By default, `setup` contains a `"HELLO 3"` command.
*/
request setup = detail::make_hello_request();
};
} // namespace boost::redis

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
//
// 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_CONNECT_FSM_HPP
#define BOOST_REDIS_CONNECT_FSM_HPP
#include <boost/redis/config.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for redis_stream::async_connect, as a finite state machine
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};
};
// What should we do next?
enum class connect_action_type
{
unix_socket_close, // Close the UNIX socket, to discard state
unix_socket_connect, // Connect to the UNIX socket
tcp_resolve, // Name resolution
tcp_connect, // TCP connect
ssl_stream_reset, // Re-create the SSL stream, to discard state
ssl_handshake, // SSL handshake
done, // Complete the async op
};
struct connect_action {
connect_action_type type;
system::error_code ec;
connect_action(connect_action_type type) noexcept
: type{type}
{ }
connect_action(system::error_code ec) noexcept
: type{connect_action_type::done}
, ec{ec}
{ }
};
class connect_fsm {
int resume_point_{0};
const config* cfg_{nullptr};
buffered_logger* lgr_{nullptr};
public:
connect_fsm(const config& cfg, buffered_logger& lgr) noexcept
: cfg_(&cfg)
, lgr_(&lgr)
{ }
const config& get_config() const { return *cfg_; }
connect_action resume(
system::error_code ec,
const asio::ip::tcp::resolver::results_type& resolver_results,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
connect_action resume(
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
connect_action resume(
system::error_code ec,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
}; // namespace boost::redis::detail
} // namespace boost::redis::detail
#endif

View File

@@ -1,54 +0,0 @@
/* 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_CONNECTION_LOGGER_HPP
#define BOOST_REDIS_CONNECTION_LOGGER_HPP
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
#include <string_view>
namespace boost::redis::detail {
// Wraps a logger and a string buffer for re-use, and provides
// utility functions to format the log messages that we use.
// The long-term trend will be moving most of this class to finite state
// machines as we write them
class connection_logger {
logger logger_;
std::string msg_;
public:
connection_logger(logger&& logger) noexcept
: logger_(std::move(logger))
{ }
void reset(logger&& logger) { logger_ = std::move(logger); }
void on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res);
void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep);
void on_connect(system::error_code const& ec, std::string_view unix_socket_ep);
void on_ssl_handshake(system::error_code const& ec);
void on_write(system::error_code const& ec, std::size_t n);
void on_fsm_resume(reader_fsm::action const& action);
void on_hello(system::error_code const& ec, generic_response const& resp);
void log(logger::level lvl, std::string_view msg);
void log(logger::level lvl, std::string_view op, system::error_code const& ec);
void trace(std::string_view message) { log(logger::level::debug, message); }
void trace(std::string_view op, system::error_code const& ec)
{
log(logger::level::debug, op, ec);
}
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_LOGGER_HPP

View File

@@ -0,0 +1,34 @@
//
// 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_CONNECTION_STATE_HPP
#define BOOST_REDIS_CONNECTION_STATE_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <string>
namespace boost::redis::detail {
// Contains all the members in connection that don't depend on the Executor.
// Makes implementing sans-io algorithms easier
struct connection_state {
buffered_logger logger;
config cfg{};
multiplexer mpx{};
std::string setup_diagnostic{};
request ping_req{};
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -29,7 +29,6 @@ enum class exec_action_type
done, // Call the final handler
notify_writer, // Notify the writer task
wait_for_response, // Wait to be notified
cancel_run, // Cancel the connection's run operation
};
class exec_action {

View File

@@ -1,193 +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_HEALTH_CHECKER_HPP
#define BOOST_REDIS_HEALTH_CHECKER_HPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_logger.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
namespace boost::redis::detail {
template <class HealthChecker, class Connection>
class ping_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER(coro_) for (;;)
{
if (checker_->ping_interval_ == std::chrono::seconds::zero()) {
conn_->logger_.trace("ping_op (1): timeout disabled.");
BOOST_ASIO_CORO_YIELD
asio::post(std::move(self));
self.complete({});
return;
}
if (checker_->checker_has_exited_) {
conn_->logger_.trace("ping_op (2): checker has exited.");
self.complete({});
return;
}
BOOST_ASIO_CORO_YIELD
conn_->async_exec(checker_->req_, any_adapter(checker_->resp_), std::move(self));
if (ec) {
conn_->logger_.trace("ping_op (3)", ec);
checker_->wait_timer_.cancel();
self.complete(ec);
return;
}
// Wait before pinging again.
checker_->ping_timer_.expires_after(checker_->ping_interval_);
BOOST_ASIO_CORO_YIELD
checker_->ping_timer_.async_wait(std::move(self));
if (ec) {
conn_->logger_.trace("ping_op (4)", ec);
self.complete(ec);
return;
}
}
}
};
template <class HealthChecker, class Connection>
class check_timeout_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER(coro_) for (;;)
{
if (checker_->ping_interval_ == std::chrono::seconds::zero()) {
conn_->logger_.trace("check_timeout_op (1): timeout disabled.");
BOOST_ASIO_CORO_YIELD
asio::post(std::move(self));
self.complete({});
return;
}
checker_->wait_timer_.expires_after(2 * checker_->ping_interval_);
BOOST_ASIO_CORO_YIELD
checker_->wait_timer_.async_wait(std::move(self));
if (ec) {
conn_->logger_.trace("check_timeout_op (2)", ec);
self.complete(ec);
return;
}
if (checker_->resp_.has_error()) {
// TODO: Log the error.
conn_->logger_.trace("check_timeout_op (3): Response error.");
self.complete({});
return;
}
if (checker_->resp_.value().empty()) {
conn_->logger_.trace("check_timeout_op (4): pong timeout.");
checker_->ping_timer_.cancel();
conn_->cancel(operation::run);
checker_->checker_has_exited_ = true;
self.complete(error::pong_timeout);
return;
}
if (checker_->resp_.has_value()) {
checker_->resp_.value().clear();
}
}
}
};
template <class Executor>
class health_checker {
private:
using timer_type = asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
public:
health_checker(Executor ex)
: ping_timer_{ex}
, wait_timer_{ex}
{
req_.push("PING", "Boost.Redis");
}
void set_config(config const& cfg)
{
req_.clear();
req_.push("PING", cfg.health_check_id);
ping_interval_ = cfg.health_check_interval;
}
void cancel()
{
ping_timer_.cancel();
wait_timer_.cancel();
}
template <class Connection, class CompletionToken>
auto async_ping(Connection& conn, CompletionToken token)
{
return asio::async_compose<CompletionToken, void(system::error_code)>(
ping_op<health_checker, Connection>{this, &conn},
token,
conn,
ping_timer_);
}
template <class Connection, class CompletionToken>
auto async_check_timeout(Connection& conn, CompletionToken token)
{
checker_has_exited_ = false;
return asio::async_compose<CompletionToken, void(system::error_code)>(
check_timeout_op<health_checker, Connection>{this, &conn},
token,
conn,
wait_timer_);
}
private:
template <class, class> friend class ping_op;
template <class, class> friend class check_timeout_op;
timer_type ping_timer_;
timer_type wait_timer_;
redis::request req_;
redis::generic_response resp_;
std::chrono::steady_clock::duration ping_interval_ = std::chrono::seconds{5};
bool checker_has_exited_ = false;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_HEALTH_CHECKER_HPP

View File

@@ -1,34 +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_HELPER_HPP
#define BOOST_REDIS_HELPER_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail {
template <class T>
auto is_cancelled(T const& self)
{
return self.get_cancellation_state().cancelled() != asio::cancellation_type_t::none;
}
#define BOOST_REDIS_CHECK_OP0(X) \
if (ec || redis::detail::is_cancelled(self)) { \
X self.complete(!!ec ? ec : asio::error::operation_aborted); \
return; \
}
#define BOOST_REDIS_CHECK_OP1(X) \
if (ec || redis::detail::is_cancelled(self)) { \
X self.complete(!!ec ? ec : asio::error::operation_aborted, {}); \
return; \
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_HELPER_HPP

View File

@@ -10,17 +10,18 @@
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/usage.hpp>
#include <boost/asio/experimental/channel.hpp>
#include <boost/system/error_code.hpp>
#include <algorithm>
#include <cstddef>
#include <deque>
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
#include <utility>
@@ -30,16 +31,19 @@ class request;
namespace detail {
using tribool = std::optional<bool>;
struct multiplexer {
using adapter_type = std::function<void(resp3::node_view const&, system::error_code&)>;
using pipeline_adapter_type = std::function<
void(std::size_t, resp3::node_view const&, system::error_code&)>;
// Return type of the multiplexer::consume_next function
enum class consume_result
{
needs_more, // consume_next didn't have enough data
got_response, // got a response to a regular command, vs. a push
got_push, // got a response to a push
};
class multiplexer {
public:
struct elem {
public:
explicit elem(request const& req, pipeline_adapter_type adapter);
explicit elem(request const& req, any_adapter adapter);
void set_done_callback(std::function<void()> f) noexcept { done_ = std::move(f); };
@@ -91,7 +95,17 @@ struct multiplexer {
auto commit_response(std::size_t read_size) -> void;
auto get_adapter() -> adapter_type& { return adapter_; }
auto get_adapter() -> any_adapter& { return adapter_; }
// Marks the element as an abandoned request. An abandoned request
// won't cause problems when its response arrives, but that response will be ignored.
void mark_abandoned();
[[nodiscard]]
bool is_abandoned() const
{
return !req_;
}
private:
enum class status
@@ -103,8 +117,7 @@ struct multiplexer {
};
request const* req_;
adapter_type adapter_;
any_adapter adapter_;
std::function<void()> done_;
// Contains the number of commands that haven't been read yet.
@@ -115,21 +128,30 @@ struct multiplexer {
std::size_t read_size_;
};
auto remove(std::shared_ptr<elem> const& ptr) -> bool;
multiplexer();
// To be called before a write operation. Coalesces all available requests
// into a single buffer. Returns the number of coalesced requests.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
[[nodiscard]]
auto prepare_write() -> std::size_t;
// Returns the number of requests that have been released because
// they don't have a response e.g. SUBSCRIBE.
auto commit_write() -> std::size_t;
// To be called after a write operation.
// Returns true once all the bytes in the buffer generated by prepare_write
// have been written.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
auto commit_write(std::size_t bytes_written) -> bool;
// If the tribool contains no value more data is needed, otherwise
// if the value is true the message consumed is a push.
// To be called after a successful read operation.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
[[nodiscard]]
auto consume_next(system::error_code& ec) -> std::pair<tribool, std::size_t>;
auto consume(system::error_code& ec) -> std::pair<consume_result, std::size_t>;
auto add(std::shared_ptr<elem> const& ptr) -> void;
void cancel(std::shared_ptr<elem> const& ptr);
auto reset() -> void;
[[nodiscard]]
@@ -138,45 +160,39 @@ struct multiplexer {
return parser_;
}
//[[nodiscard]]
auto cancel_waiting() -> std::size_t;
//[[nodiscard]]
auto cancel_on_conn_lost() -> std::size_t;
// To be called exactly once to clean up state after a connection becomes unhealthy.
// Requests are canceled or returned to the waiting state to be re-sent to the server,
// depending on their configuration. After this function is called, prepare_write,
// commit_write and consume_next must not be called until a reset() happens.
// Otherwise, race conditions like the following might happen
// (see https://github.com/boostorg/redis/pull/309 and https://github.com/boostorg/redis/issues/181):
//
// - This function runs and cancels a request, then consume_next runs. It tries to access
// a request and adapter that might have been destroyed.
// - This function runs and returns a request to waiting, then prepare_write runs.
// It incorrectly sets the request state to staged, causing de synchronization between requests and responses.
void cancel_on_conn_lost();
[[nodiscard]]
auto get_cancel_run_state() const noexcept -> bool
auto get_write_buffer() const noexcept -> std::string_view
{
return cancel_run_called_;
return std::string_view{write_buffer_}.substr(write_offset_);
}
[[nodiscard]]
auto get_write_buffer() noexcept -> std::string_view
{
return std::string_view{write_buffer_};
}
auto get_prepared_read_buffer() noexcept -> read_buffer::span_type;
[[nodiscard]]
auto get_read_buffer() noexcept -> std::string&
{
return read_buffer_;
}
auto prepare_read() noexcept -> system::error_code;
void commit_read(std::size_t read_size);
[[nodiscard]]
auto get_read_buffer() const noexcept -> std::string const&
{
return read_buffer_;
}
auto get_read_buffer_size() const noexcept -> std::size_t;
// TODO: Change signature to receive an adapter instead of a
// response.
template <class Response>
void set_receive_response(Response& response)
{
using namespace boost::redis::adapter;
auto g = boost_redis_adapt(response);
receive_adapter_ = adapter::detail::make_adapter_wrapper(g);
}
void set_receive_adapter(any_adapter adapter);
[[nodiscard]]
auto get_usage() const noexcept -> usage
@@ -184,35 +200,32 @@ struct multiplexer {
return usage_;
}
[[nodiscard]]
auto is_writing() const noexcept -> bool;
void set_config(config const& cfg);
private:
[[nodiscard]]
auto is_waiting_response() const noexcept -> bool;
void commit_usage(bool is_push, read_buffer::consume_result res);
[[nodiscard]]
auto on_finish_parsing(bool is_push) -> std::size_t;
auto is_next_push(std::string_view data) const noexcept -> bool;
// Completes requests that don't expect a response
void release_push_requests();
[[nodiscard]]
auto is_next_push() const noexcept -> bool;
consume_result consume_impl(system::error_code& ec);
// Releases the number of requests that have been released.
[[nodiscard]]
auto release_push_requests() -> std::size_t;
std::string read_buffer_;
read_buffer read_buffer_;
std::string write_buffer_;
std::size_t write_offset_{}; // how many bytes of the write buffer have been written?
std::deque<std::shared_ptr<elem>> reqs_;
resp3::parser parser_{};
bool on_push_ = false;
bool cancel_run_called_ = false;
usage usage_;
adapter_type receive_adapter_;
any_adapter receive_adapter_;
};
auto make_elem(request const& req, multiplexer::pipeline_adapter_type adapter)
-> std::shared_ptr<multiplexer::elem>;
auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>;
} // namespace detail
} // namespace boost::redis

View File

@@ -0,0 +1,69 @@
/* 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_READ_BUFFER_HPP
#define BOOST_REDIS_READ_BUFFER_HPP
#include <boost/core/span.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string_view>
#include <utility>
#include <vector>
namespace boost::redis::detail {
class read_buffer {
public:
using span_type = span<char>;
struct consume_result {
std::size_t consumed;
std::size_t rotated;
};
// See config.hpp for the meaning of these parameters.
struct config {
std::size_t read_buffer_append_size = 4096u;
std::size_t max_read_size = static_cast<std::size_t>(-1);
};
// Prepare the buffer to receive more data.
[[nodiscard]]
auto prepare() -> system::error_code;
[[nodiscard]]
auto get_prepared() noexcept -> span_type;
void commit(std::size_t read_size);
[[nodiscard]]
auto get_commited() const noexcept -> std::string_view;
void clear();
// Consumes committed data by rotating the remaining data to the
// front of the buffer.
auto consume(std::size_t size) -> consume_result;
void reserve(std::size_t n);
friend bool operator==(read_buffer const& lhs, read_buffer const& rhs);
friend bool operator!=(read_buffer const& lhs, read_buffer const& rhs);
void set_config(config const& cfg) noexcept { cfg_ = cfg; };
private:
config cfg_ = config{};
std::vector<char> buffer_;
std::size_t append_buf_begin_ = 0;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_READ_BUFFER_HPP

View File

@@ -7,46 +7,89 @@
#ifndef BOOST_REDIS_READER_FSM_HPP
#define BOOST_REDIS_READER_FSM_HPP
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <cstddef>
namespace boost::redis::detail {
class read_buffer;
class reader_fsm {
public:
struct action {
class action {
public:
enum class type
{
setup_cancellation,
append_some,
needs_more,
read_some,
notify_push_receiver,
cancel_run,
done,
};
type type_ = type::setup_cancellation;
std::size_t push_size_ = 0;
system::error_code ec_ = {};
action(system::error_code ec) noexcept
: type_(type::done)
, ec_(ec)
{ }
static action read_some(std::chrono::steady_clock::duration timeout) { return {timeout}; }
static action notify_push_receiver(std::size_t bytes) { return {bytes}; }
type get_type() const { return type_; }
system::error_code error() const
{
BOOST_ASSERT(type_ == type::done);
return ec_;
}
std::chrono::steady_clock::duration timeout() const
{
BOOST_ASSERT(type_ == type::read_some);
return timeout_;
}
std::size_t push_size() const
{
BOOST_ASSERT(type_ == type::notify_push_receiver);
return push_size_;
}
private:
action(std::size_t push_size) noexcept
: type_(type::notify_push_receiver)
, push_size_(push_size)
{ }
action(std::chrono::steady_clock::duration t) noexcept
: type_(type::read_some)
, timeout_(t)
{ }
type type_;
union {
system::error_code ec_;
std::chrono::steady_clock::duration timeout_;
std::size_t push_size_{};
};
};
explicit reader_fsm(multiplexer& mpx) noexcept;
action resume(
connection_state& st,
std::size_t bytes_read,
system::error_code ec,
asio::cancellation_type_t /*cancel_state*/);
asio::cancellation_type_t cancel_state);
reader_fsm() = default;
private:
int resume_point_{0};
action action_after_resume_;
action::type next_read_type_ = action::type::append_some;
multiplexer* mpx_ = nullptr;
std::pair<tribool, std::size_t> res_{std::make_pair(std::nullopt, 0)};
std::pair<consume_result, std::size_t> res_{consume_result::needs_more, 0u};
};
} // namespace boost::redis::detail

View File

@@ -8,8 +8,9 @@
#define BOOST_REDIS_REDIS_STREAM_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_logger.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/basic_waitable_timer.hpp>
#include <boost/asio/cancel_after.hpp>
@@ -31,14 +32,6 @@ namespace boost {
namespace redis {
namespace detail {
// What transport is redis_stream using?
enum class transport_type
{
tcp, // plaintext TCP
tcp_tls, // TLS over TCP
unix_socket, // UNIX domain sockets
};
template <class Executor>
class redis_stream {
asio::ssl::context ssl_ctx_;
@@ -48,135 +41,103 @@ class redis_stream {
asio::basic_stream_socket<asio::local::stream_protocol, Executor> unix_socket_;
#endif
typename asio::steady_timer::template rebind_executor<Executor>::other timer_;
transport_type transport_{transport_type::tcp};
bool ssl_stream_used_{false};
redis_stream_state st_;
void reset_stream() { stream_ = {resolv_.get_executor(), ssl_ctx_}; }
static transport_type transport_from_config(const config& cfg)
{
if (cfg.unix_socket.empty()) {
if (cfg.use_ssl) {
return transport_type::tcp_tls;
} else {
return transport_type::tcp;
}
} else {
BOOST_ASSERT(!cfg.use_ssl);
return transport_type::unix_socket;
}
}
struct connect_op {
redis_stream& obj;
const config* cfg;
connection_logger* lgr;
asio::coroutine coro{};
connect_fsm fsm_;
// This overload will be used for connects. We only need the endpoint
// for logging, so log it and call the coroutine
template <class Self>
void execute_action(Self& self, connect_action act)
{
const auto& cfg = fsm_.get_config();
switch (act.type) {
case connect_action_type::unix_socket_close:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
{
system::error_code ec;
obj.unix_socket_.close(ec);
(*this)(self, ec); // This is a sync action
}
#else
BOOST_ASSERT(false);
#endif
return;
case connect_action_type::unix_socket_connect:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
obj.unix_socket_.async_connect(
cfg.unix_socket,
asio::cancel_after(obj.timer_, cfg.connect_timeout, std::move(self)));
#else
BOOST_ASSERT(false);
#endif
return;
case connect_action_type::tcp_resolve:
obj.resolv_.async_resolve(
cfg.addr.host,
cfg.addr.port,
asio::cancel_after(obj.timer_, cfg.resolve_timeout, std::move(self)));
return;
case connect_action_type::ssl_stream_reset:
obj.reset_stream();
// this action does not require yielding. Execute the next action immediately
(*this)(self);
return;
case connect_action_type::ssl_handshake:
obj.stream_.async_handshake(
asio::ssl::stream_base::client,
asio::cancel_after(obj.timer_, cfg.ssl_handshake_timeout, std::move(self)));
return;
case connect_action_type::done: self.complete(act.ec); break;
// Connect should use the specialized handler, where resolver results are available
case connect_action_type::tcp_connect:
default: BOOST_ASSERT(false);
}
}
// This overload will be used for connects
template <class Self>
void operator()(
Self& self,
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint)
{
lgr->on_connect(ec, selected_endpoint);
(*this)(self, ec);
auto act = fsm_.resume(
ec,
selected_endpoint,
obj.st_,
self.get_cancellation_state().cancelled());
execute_action(self, act);
}
// This overload will be used for resolves
template <class Self>
void operator()(
Self& self,
system::error_code ec = {},
asio::ip::tcp::resolver::results_type resolver_results = {})
system::error_code ec,
asio::ip::tcp::resolver::results_type endpoints)
{
BOOST_ASIO_CORO_REENTER(coro)
{
// Record the transport that we will be using
obj.transport_ = transport_from_config(*cfg);
if (obj.transport_ == transport_type::unix_socket) {
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
// Directly connect to the socket
BOOST_ASIO_CORO_YIELD
obj.unix_socket_.async_connect(
cfg->unix_socket,
asio::cancel_after(obj.timer_, cfg->connect_timeout, std::move(self)));
// Log it
lgr->on_connect(ec, cfg->unix_socket);
// If this failed, we can't continue
if (ec) {
self.complete(ec == asio::error::operation_aborted ? error::connect_timeout : ec);
return;
}
#else
BOOST_ASSERT(false);
#endif
} else {
// ssl::stream doesn't support being re-used. If we're to use
// TLS and the stream has been used, re-create it.
// Must be done before anything else is done on the stream
if (cfg->use_ssl && obj.ssl_stream_used_)
obj.reset_stream();
BOOST_ASIO_CORO_YIELD
obj.resolv_.async_resolve(
cfg->addr.host,
cfg->addr.port,
asio::cancel_after(obj.timer_, cfg->resolve_timeout, std::move(self)));
// Log it
lgr->on_resolve(ec, resolver_results);
// If this failed, we can't continue
if (ec) {
self.complete(ec == asio::error::operation_aborted ? error::resolve_timeout : ec);
return;
}
// Connect to the address that the resolver provided us
BOOST_ASIO_CORO_YIELD
asio::async_connect(
obj.stream_.next_layer(),
std::move(resolver_results),
asio::cancel_after(obj.timer_, cfg->connect_timeout, std::move(self)));
// Note: logging is performed in the specialized operator() function.
// If this failed, we can't continue
if (ec) {
self.complete(ec == asio::error::operation_aborted ? error::connect_timeout : ec);
return;
}
if (cfg->use_ssl) {
// Mark the SSL stream as used
obj.ssl_stream_used_ = true;
// If we were configured to use TLS, perform the handshake
BOOST_ASIO_CORO_YIELD
obj.stream_.async_handshake(
asio::ssl::stream_base::client,
asio::cancel_after(obj.timer_, cfg->ssl_handshake_timeout, std::move(self)));
lgr->on_ssl_handshake(ec);
// If this failed, we can't continue
if (ec) {
self.complete(
ec == asio::error::operation_aborted ? error::ssl_handshake_timeout : ec);
return;
}
}
}
// Done
self.complete(system::error_code());
auto act = fsm_.resume(ec, endpoints, obj.st_, self.get_cancellation_state().cancelled());
if (act.type == connect_action_type::tcp_connect) {
asio::async_connect(
obj.stream_.next_layer(),
std::move(endpoints),
asio::cancel_after(obj.timer_, fsm_.get_config().connect_timeout, std::move(self)));
} else {
execute_action(self, act);
}
}
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
auto act = fsm_.resume(ec, obj.st_, self.get_cancellation_state().cancelled());
execute_action(self, act);
}
};
public:
@@ -199,7 +160,7 @@ public:
bool is_open() const
{
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
if (transport_ == transport_type::unix_socket)
if (st_.type == transport_type::unix_socket)
return unix_socket_.is_open();
#endif
return stream_.next_layer().is_open();
@@ -209,10 +170,10 @@ public:
// I/O
template <class CompletionToken>
auto async_connect(const config* cfg, connection_logger* l, CompletionToken&& token)
auto async_connect(const config& cfg, buffered_logger& l, CompletionToken&& token)
{
return asio::async_compose<CompletionToken, void(system::error_code)>(
connect_op{*this, cfg, l},
connect_op{*this, connect_fsm(cfg, l)},
token);
}
@@ -220,7 +181,7 @@ public:
template <class ConstBufferSequence, class CompletionToken>
void async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token)
{
switch (transport_) {
switch (st_.type) {
case transport_type::tcp:
{
stream_.next_layer().async_write_some(buffers, std::forward<CompletionToken>(token));
@@ -245,7 +206,7 @@ public:
template <class MutableBufferSequence, class CompletionToken>
void async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token)
{
switch (transport_) {
switch (st_.type) {
case transport_type::tcp:
{
return stream_.next_layer().async_read_some(
@@ -269,19 +230,11 @@ public:
}
}
// Cleanup
// Cancels resolve operations. Resolve operations don't support per-operation
// cancellation, but resolvers have a cancel() function. Resolve operations are
// in general blocking and run in a separate thread. cancel() has effect only
// if the operation hasn't started yet. Still, trying is better than nothing
void cancel_resolve() { resolv_.cancel(); }
void close()
{
system::error_code ec;
if (stream_.next_layer().is_open())
stream_.next_layer().close(ec);
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
if (unix_socket_.is_open())
unix_socket_.close(ec);
#endif
}
};
} // namespace detail

View File

@@ -1,114 +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_RUNNER_HPP
#define BOOST_REDIS_RUNNER_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_logger.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/coroutine.hpp>
#include <string>
namespace boost::redis::detail {
void push_hello(config const& cfg, request& req);
// TODO: Can we avoid this whole function whose only purpose is to
// check for an error in the hello response and complete with an error
// so that the parallel group that starts it can exit?
template <class Handshaker, class Connection>
struct hello_op {
Handshaker* handshaker_ = nullptr;
Connection* conn_ = nullptr;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER(coro_)
{
handshaker_->add_hello();
BOOST_ASIO_CORO_YIELD
conn_->async_exec(
handshaker_->hello_req_,
any_adapter(handshaker_->hello_resp_),
std::move(self));
conn_->logger_.on_hello(ec, handshaker_->hello_resp_);
if (ec) {
conn_->cancel(operation::run);
self.complete(ec);
return;
}
if (handshaker_->has_error_in_response()) {
conn_->cancel(operation::run);
self.complete(error::resp3_hello);
return;
}
self.complete({});
}
}
};
template <class Executor>
class resp3_handshaker {
public:
void set_config(config const& cfg) { cfg_ = cfg; }
template <class Connection, class CompletionToken>
auto async_hello(Connection& conn, CompletionToken token)
{
return asio::async_compose<CompletionToken, void(system::error_code)>(
hello_op<resp3_handshaker, Connection>{this, &conn},
token,
conn);
}
private:
template <class, class> friend struct hello_op;
void add_hello()
{
hello_req_.clear();
if (hello_resp_.has_value())
hello_resp_.value().clear();
push_hello(cfg_, hello_req_);
}
bool has_error_in_response() const noexcept
{
if (!hello_resp_.has_value())
return true;
auto f = [](auto const& e) {
switch (e.data_type) {
case resp3::type::simple_error:
case resp3::type::blob_error: return true;
default: return false;
}
};
return std::any_of(std::cbegin(hello_resp_.value()), std::cend(hello_resp_.value()), f);
}
request hello_req_;
generic_response hello_resp_;
config cfg_;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_RUNNER_HPP

View File

@@ -0,0 +1,29 @@
/* 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_RESP3_TYPE_TO_ERROR_HPP
#define BOOST_RESP3_TYPE_TO_ERROR_HPP
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert.hpp>
namespace boost::redis::detail {
inline error resp3_type_to_error(resp3::type t)
{
switch (t) {
case resp3::type::simple_error: return error::resp3_simple_error;
case resp3::type::blob_error: return error::resp3_blob_error;
case resp3::type::null: return error::resp3_null;
default: BOOST_ASSERT_MSG(false, "Unexpected data type."); return error::resp3_simple_error;
}
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP

View File

@@ -0,0 +1,62 @@
//
// 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_RUN_FSM_HPP
#define BOOST_REDIS_RUN_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for async_run, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
// What should we do next?
enum class run_action_type
{
done, // Call the final handler
immediate, // Call asio::async_immediate
connect, // Transport connection establishment
parallel_group, // Run the reader, writer and friends
cancel_receive, // Cancel the receiver channel
wait_for_reconnection, // Sleep for the reconnection period
};
struct run_action {
run_action_type type;
system::error_code ec;
run_action(run_action_type type) noexcept
: type{type}
{ }
run_action(system::error_code ec) noexcept
: type{run_action_type::done}
, ec{ec}
{ }
};
class run_fsm {
int resume_point_{0};
system::error_code stored_ec_;
public:
run_fsm() = default;
run_action resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -0,0 +1,92 @@
//
// 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_WRITER_FSM_HPP
#define BOOST_REDIS_WRITER_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <cstddef>
// Sans-io algorithm for the writer task, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
// What should we do next?
enum class writer_action_type
{
done, // Call the final handler
write_some, // Issue a write on the stream
wait, // Wait until there is data to be written
};
class writer_action {
writer_action_type type_;
union {
system::error_code ec_;
std::chrono::steady_clock::duration timeout_;
};
writer_action(writer_action_type type, std::chrono::steady_clock::duration t) noexcept
: type_{type}
, timeout_{t}
{ }
public:
writer_action_type type() const { return type_; }
writer_action(system::error_code ec) noexcept
: type_{writer_action_type::done}
, ec_{ec}
{ }
static writer_action write_some(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::write_some, timeout};
}
static writer_action wait(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::wait, timeout};
}
system::error_code error() const
{
BOOST_ASSERT(type_ == writer_action_type::done);
return ec_;
}
std::chrono::steady_clock::duration timeout() const
{
BOOST_ASSERT(type_ == writer_action_type::write_some || type_ == writer_action_type::wait);
return timeout_;
}
};
class writer_fsm {
int resume_point_{0};
public:
writer_fsm() = default;
writer_action resume(
connection_state& st,
system::error_code ec,
std::size_t bytes_written,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -68,7 +68,7 @@ enum class error
/// Connect timeout
connect_timeout,
/// Connect timeout
/// The server didn't answer the health checks on time and didn't send any data during the health check period.
pong_timeout,
/// SSL handshake timeout
@@ -80,7 +80,7 @@ enum class error
/// Incompatible node depth.
incompatible_node_depth,
/// Resp3 hello command error
/// The setup request sent during connection establishment failed (the name is historical).
resp3_hello,
/// The configuration specified a UNIX socket address, but UNIX sockets are not supported by the system.
@@ -88,6 +88,12 @@ enum class error
/// The configuration specified UNIX sockets with SSL, which is not supported.
unix_sockets_ssl_unsupported,
/// Reading data from the socket would exceed the maximum size allowed of the read buffer.
exceeds_maximum_read_buffer_size,
/// Timeout while writing data to the server.
write_timeout,
};
/**

View File

@@ -0,0 +1,235 @@
//
// 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 <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/assert.hpp>
#include <string>
namespace boost::redis::detail {
// Logging
inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to)
{
// This formatting is inspired by Asio's endpoint operator<<
const auto& addr = ep.address();
if (addr.is_v6())
to += '[';
to += addr.to_string();
if (addr.is_v6())
to += ']';
to += ':';
to += std::to_string(ep.port());
}
template <>
struct log_traits<asio::ip::tcp::endpoint> {
static inline void log(std::string& to, const asio::ip::tcp::endpoint& value)
{
format_tcp_endpoint(value, to);
}
};
template <>
struct log_traits<asio::ip::tcp::resolver::results_type> {
static inline void log(std::string& to, const asio::ip::tcp::resolver::results_type& value)
{
auto iter = value.cbegin();
auto end = value.cend();
if (iter != end) {
format_tcp_endpoint(iter->endpoint(), to);
++iter;
for (; iter != end; ++iter) {
to += ", ";
format_tcp_endpoint(iter->endpoint(), to);
}
}
}
};
inline transport_type transport_from_config(const config& cfg)
{
if (cfg.unix_socket.empty()) {
if (cfg.use_ssl) {
return transport_type::tcp_tls;
} else {
return transport_type::tcp;
}
} else {
BOOST_ASSERT(!cfg.use_ssl);
return transport_type::unix_socket;
}
}
inline system::error_code translate_timeout_error(
system::error_code io_ec,
asio::cancellation_type_t cancel_state,
error code_if_cancelled)
{
// Translates cancellations and timeout errors into a single error_code.
// - Cancellation state set, and an I/O error: the entire operation was cancelled.
// The I/O code (probably operation_aborted) is appropriate.
// - Cancellation state set, and no I/O error: same as above, but the cancellation
// arrived after the operation completed and before the handler was called. Set the code here.
// - No cancellation state set, I/O error set to operation_aborted: since we use cancel_after,
// this means a timeout.
// - Otherwise, respect the I/O error.
if ((cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none) {
return io_ec ? io_ec : asio::error::operation_aborted;
}
return io_ec == asio::error::operation_aborted ? code_if_cancelled : io_ec;
}
connect_action connect_fsm::resume(
system::error_code ec,
const asio::ip::tcp::resolver::results_type& resolver_results,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::resolve_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Error resolving the server hostname: ", ec);
} else {
log_info(*lgr_, "Resolve results: ", resolver_results);
}
// Delegate to the regular resume function
return resume(ec, st, cancel_state);
}
connect_action connect_fsm::resume(
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::connect_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Failed to connect to the server: ", ec);
} else {
log_info(*lgr_, "Connected to ", selected_endpoint);
}
// Delegate to the regular resume function
return resume(ec, st, cancel_state);
}
connect_action connect_fsm::resume(
system::error_code ec,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Record the transport that we will be using
st.type = transport_from_config(*cfg_);
if (st.type == transport_type::unix_socket) {
// Reset the socket, to discard any previous state. Ignore any errors
BOOST_REDIS_YIELD(resume_point_, 1, connect_action_type::unix_socket_close)
// Connect to the socket
BOOST_REDIS_YIELD(resume_point_, 2, connect_action_type::unix_socket_connect)
// Fix error codes. If we were cancelled and the code is operation_aborted,
// it is because per-operation cancellation was activated. If we were not cancelled
// but the operation failed with operation_aborted, it's a timeout.
// Also check for cancellations that didn't cause a failure
ec = translate_timeout_error(ec, cancel_state, error::connect_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Failed to connect to the server: ", ec);
} else {
log_info(*lgr_, "Connected to ", cfg_->unix_socket);
}
// If this failed, we can't continue
if (ec) {
return ec;
}
// Done
return system::error_code();
} else {
// ssl::stream doesn't support being re-used. If we're to use
// TLS and the stream has been used, re-create it.
// Must be done before anything else is done on the stream.
// We don't need to close the TCP socket if using plaintext TCP
// because range-connect closes open sockets, while individual connect doesn't
if (cfg_->use_ssl && st.ssl_stream_used) {
BOOST_REDIS_YIELD(resume_point_, 3, connect_action_type::ssl_stream_reset)
}
// Resolve names. The continuation needs access to the returned
// endpoints, and is a specialized resume() that will call this function
BOOST_REDIS_YIELD(resume_point_, 4, connect_action_type::tcp_resolve)
// If this failed, we can't continue (error code translation already performed here)
if (ec) {
return ec;
}
// Now connect to the endpoints returned by the resolver.
// This has a specialized resume(), too
BOOST_REDIS_YIELD(resume_point_, 5, connect_action_type::tcp_connect)
// If this failed, we can't continue (error code translation already performed here)
if (ec) {
return ec;
}
if (cfg_->use_ssl) {
// Mark the SSL stream as used
st.ssl_stream_used = true;
// Perform the TLS handshake
BOOST_REDIS_YIELD(resume_point_, 6, connect_action_type::ssl_handshake)
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::ssl_handshake_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Failed to perform SSL handshake: ", ec);
} else {
log_info(*lgr_, "Successfully performed SSL handshake");
}
// If this failed, we can't continue
if (ec) {
return ec;
}
}
// Done
return system::error_code();
}
}
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail

View File

@@ -1,216 +0,0 @@
/* 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 <boost/redis/detail/connection_logger.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
#include <string>
namespace boost::redis::detail {
#define BOOST_REDIS_READER_SWITCH_CASE(elem) \
case reader_fsm::action::type::elem: return "reader_fsm::action::type::" #elem
#define BOOST_REDIS_EXEC_SWITCH_CASE(elem) \
case exec_action_type::elem: return "exec_action_type::" #elem
auto to_string(reader_fsm::action::type t) noexcept -> char const*
{
switch (t) {
BOOST_REDIS_READER_SWITCH_CASE(setup_cancellation);
BOOST_REDIS_READER_SWITCH_CASE(append_some);
BOOST_REDIS_READER_SWITCH_CASE(needs_more);
BOOST_REDIS_READER_SWITCH_CASE(notify_push_receiver);
BOOST_REDIS_READER_SWITCH_CASE(cancel_run);
BOOST_REDIS_READER_SWITCH_CASE(done);
default: return "action::type::<invalid type>";
}
}
auto to_string(exec_action_type t) noexcept -> char const*
{
switch (t) {
BOOST_REDIS_EXEC_SWITCH_CASE(setup_cancellation);
BOOST_REDIS_EXEC_SWITCH_CASE(immediate);
BOOST_REDIS_EXEC_SWITCH_CASE(done);
BOOST_REDIS_EXEC_SWITCH_CASE(notify_writer);
BOOST_REDIS_EXEC_SWITCH_CASE(wait_for_response);
BOOST_REDIS_EXEC_SWITCH_CASE(cancel_run);
default: return "exec_action_type::<invalid type>";
}
}
inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to)
{
// This formatting is inspired by Asio's endpoint operator<<
const auto& addr = ep.address();
if (addr.is_v6())
to += '[';
to += addr.to_string();
if (addr.is_v6())
to += ']';
to += ':';
to += std::to_string(ep.port());
}
inline void format_error_code(system::error_code ec, std::string& to)
{
// Using error_code::what() includes any source code info
// that the error may contain, making the messages too long.
// This implementation was taken from error_code::what()
to += ec.message();
to += " [";
to += ec.to_string();
to += ']';
}
void connection_logger::on_resolve(
system::error_code const& ec,
asio::ip::tcp::resolver::results_type const& res)
{
if (logger_.lvl < logger::level::info)
return;
if (ec) {
msg_ = "Error resolving the server hostname: ";
format_error_code(ec, msg_);
} else {
msg_ = "Resolve results: ";
auto iter = res.cbegin();
auto end = res.cend();
if (iter != end) {
format_tcp_endpoint(iter->endpoint(), msg_);
++iter;
for (; iter != end; ++iter) {
msg_ += ", ";
format_tcp_endpoint(iter->endpoint(), msg_);
}
}
}
logger_.fn(logger::level::info, msg_);
}
void connection_logger::on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep)
{
if (logger_.lvl < logger::level::info)
return;
if (ec) {
msg_ = "Failed connecting to the server: ";
format_error_code(ec, msg_);
} else {
msg_ = "Connected to ";
format_tcp_endpoint(ep, msg_);
}
logger_.fn(logger::level::info, msg_);
}
void connection_logger::on_connect(system::error_code const& ec, std::string_view unix_socket_ep)
{
if (logger_.lvl < logger::level::info)
return;
if (ec) {
msg_ = "Failed connecting to the server: ";
format_error_code(ec, msg_);
} else {
msg_ = "Connected to ";
msg_ += unix_socket_ep;
}
logger_.fn(logger::level::info, msg_);
}
void connection_logger::on_ssl_handshake(system::error_code const& ec)
{
if (logger_.lvl < logger::level::info)
return;
msg_ = "SSL handshake: ";
format_error_code(ec, msg_);
logger_.fn(logger::level::info, msg_);
}
void connection_logger::on_write(system::error_code const& ec, std::size_t n)
{
if (logger_.lvl < logger::level::info)
return;
msg_ = "writer_op: ";
if (ec) {
format_error_code(ec, msg_);
} else {
msg_ += std::to_string(n);
msg_ += " bytes written.";
}
logger_.fn(logger::level::info, msg_);
}
void connection_logger::on_fsm_resume(reader_fsm::action const& action)
{
if (logger_.lvl < logger::level::debug)
return;
std::string msg;
msg += "(";
msg += to_string(action.type_);
msg += ", ";
msg += std::to_string(action.push_size_);
msg += ", ";
msg += action.ec_.message();
msg += ")";
logger_.fn(logger::level::debug, msg);
}
void connection_logger::on_hello(system::error_code const& ec, generic_response const& resp)
{
if (logger_.lvl < logger::level::info)
return;
msg_ = "hello_op: ";
if (ec) {
format_error_code(ec, msg_);
if (resp.has_error()) {
msg_ += " (";
msg_ += resp.error().diagnostic;
msg_ += ')';
}
} else {
msg_ += "success";
}
logger_.fn(logger::level::info, msg_);
}
void connection_logger::log(logger::level lvl, std::string_view message)
{
if (logger_.lvl < lvl)
return;
logger_.fn(lvl, message);
}
void connection_logger::log(logger::level lvl, std::string_view op, system::error_code const& ec)
{
if (logger_.lvl < lvl)
return;
msg_ = op;
msg_ += ": ";
format_error_code(ec, msg_);
logger_.fn(lvl, msg_);
}
} // namespace boost::redis::detail

View File

@@ -44,12 +44,19 @@ struct error_category_impl : system::error_category {
case error::sync_receive_push_failed:
return "Can't receive server push synchronously without blocking.";
case error::incompatible_node_depth: return "Incompatible node depth.";
case error::resp3_hello: return "RESP3 handshake error (hello command).";
case error::resp3_hello:
return "The server response to the setup request sent during connection establishment "
"contains an error.";
case error::unix_sockets_unsupported:
return "The configuration specified a UNIX socket address, but UNIX sockets are not "
"supported by the system.";
case error::unix_sockets_ssl_unsupported:
return "The configuration specified UNIX sockets with SSL, which is not supported.";
case error::exceeds_maximum_read_buffer_size:
return "Reading data from the socket would exceed the maximum size allowed of the read "
"buffer.";
case error::write_timeout:
return "Timeout while writing data to the server.";
default: BOOST_ASSERT(false); return "Boost.Redis error.";
}
}

View File

@@ -18,11 +18,14 @@
namespace boost::redis::detail {
inline bool is_cancellation(asio::cancellation_type_t type)
inline bool is_partial_or_terminal_cancel(asio::cancellation_type_t type)
{
return !!(
type & (asio::cancellation_type_t::total | asio::cancellation_type_t::partial |
asio::cancellation_type_t::terminal));
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, asio::cancellation_type_t cancel_state)
@@ -63,19 +66,12 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
return act;
}
// If we're cancelled, try to remove the request from the queue. This will only
// succeed if the request is waiting (wasn't written yet)
if (is_cancellation(cancel_state) && mpx_->remove(elem_)) {
elem_.reset(); // Deallocate memory before finalizing
return exec_action{asio::error::operation_aborted};
}
// If we hit a terminal cancellation, tear down the connection.
// Otherwise, go back to waiting.
// TODO: we could likely do better here and mark the request as cancelled, removing
// the done callback and the adapter. But this requires further exploration
if (!!(cancel_state & asio::cancellation_type_t::terminal)) {
BOOST_REDIS_YIELD(resume_point_, 5, exec_action_type::cancel_run)
// 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)) {
mpx_->cancel(elem_);
elem_.reset(); // Deallocate memory before finalizing
return exec_action{asio::error::operation_aborted};
}

View File

@@ -0,0 +1,23 @@
//
// 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_IS_TERMINAL_CANCEL_HPP
#define BOOST_REDIS_IS_TERMINAL_CANCEL_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail {
constexpr bool is_terminal_cancel(asio::cancellation_type_t cancel_state)
{
return (cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none;
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,99 @@
/* 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_LOG_UTILS_HPP
#define BOOST_REDIS_LOG_UTILS_HPP
#include <boost/redis/logger.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string>
#include <string_view>
#include <type_traits>
namespace boost::redis::detail {
// Internal trait that defines how to log different types.
// The base template applies to types convertible to string_view
template <class T>
struct log_traits {
// log should convert the input value to string and append it to the supplied buffer
static inline void log(std::string& to, std::string_view value) { to += value; }
};
// Formatting size_t and error codes is shared between almost all FSMs, so it's defined here.
// Support for types used only in one FSM should be added in the relevant FSM file.
template <>
struct log_traits<std::size_t> {
static inline void log(std::string& to, std::size_t value) { to += std::to_string(value); }
};
template <>
struct log_traits<system::error_code> {
static inline void log(std::string& to, system::error_code value)
{
// Using error_code::what() includes any source code info
// that the error may contain, making the messages too long.
// This implementation was taken from error_code::what()
to += value.message();
to += " [";
to += value.to_string();
to += ']';
}
};
template <class... Args>
void format_log_args(std::string& to, const Args&... args)
{
auto dummy = {(log_traits<Args>::log(to, args), 0)...};
ignore_unused(dummy);
}
// Logs a message with the specified severity to the logger.
// Formatting won't be performed if the logger's level is inferior to lvl.
// args are stringized using log_traits, and concatenated.
template <class Arg0, class... Rest>
void log(buffered_logger& to, logger::level lvl, const Arg0& arg0, const Rest&... arg_rest)
{
// Severity check
if (to.lgr.lvl < lvl)
return;
// Optimization: if we get passed a single string, don't copy it to the buffer
if constexpr (sizeof...(Rest) == 0u && std::is_convertible_v<Arg0, std::string_view>) {
to.lgr.fn(lvl, arg0);
} else {
to.buffer.clear();
format_log_args(to.buffer, arg0, arg_rest...);
to.lgr.fn(lvl, to.buffer);
}
}
// Shorthand for each log level we use
template <class... Args>
void log_debug(buffered_logger& to, const Args&... args)
{
log(to, logger::level::debug, args...);
}
template <class... Args>
void log_info(buffered_logger& to, const Args&... args)
{
log(to, logger::level::info, args...);
}
template <class... Args>
void log_err(buffered_logger& to, const Args&... args)
{
log(to, logger::level::err, args...);
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_LOGGER_HPP

View File

@@ -5,25 +5,25 @@
*/
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <cstddef>
#include <memory>
namespace boost::redis::detail {
multiplexer::elem::elem(request const& req, pipeline_adapter_type adapter)
multiplexer::elem::elem(request const& req, any_adapter adapter)
: req_{&req}
, adapter_{}
, adapter_{std::move(adapter)}
, remaining_responses_{req.get_expected_responses()}
, status_{status::waiting}
, ec_{}
, read_size_{0}
{
adapter_ = [this, adapter](resp3::node_view const& nd, system::error_code& ec) {
auto const i = req_->get_expected_responses() - remaining_responses_;
adapter(i, nd, ec);
};
}
{ }
auto multiplexer::elem::notify_error(system::error_code ec) noexcept -> void
{
@@ -40,20 +40,45 @@ auto multiplexer::elem::commit_response(std::size_t read_size) -> void
--remaining_responses_;
}
bool multiplexer::remove(std::shared_ptr<elem> const& ptr)
void multiplexer::elem::mark_abandoned()
{
if (ptr->is_waiting()) {
reqs_.erase(std::remove(std::begin(reqs_), std::end(reqs_), ptr));
return true;
}
return false;
req_ = nullptr;
adapter_ = any_adapter(); // A default-constructed any_adapter ignores all nodes
set_done_callback([] { });
}
std::size_t multiplexer::commit_write()
multiplexer::multiplexer()
{
// We have to clear the payload right after writing it to use it
// as a flag that informs there is no ongoing write.
// Reserve some memory to avoid excessive memory allocations in
// the first reads.
read_buffer_.reserve(4096u);
}
void multiplexer::cancel(std::shared_ptr<elem> const& ptr)
{
if (ptr->is_waiting()) {
// We can safely remove it from the queue, since it hasn't been sent yet
reqs_.erase(std::remove(std::begin(reqs_), std::end(reqs_), ptr));
} else {
// Removing the request would cause trouble when the response arrived.
// Mark it as abandoned, so the response is discarded when it arrives
ptr->mark_abandoned();
}
}
bool multiplexer::commit_write(std::size_t bytes_written)
{
BOOST_ASSERT(!cancel_run_called_);
BOOST_ASSERT(bytes_written + write_offset_ <= write_buffer_.size());
usage_.bytes_sent += bytes_written;
write_offset_ += bytes_written;
// Are there still more bytes to write?
if (write_offset_ < write_buffer_.size())
return false;
// We've written all the bytes in the write buffer.
write_buffer_.clear();
// There is small optimization possible here: traverse only the
@@ -65,14 +90,18 @@ std::size_t multiplexer::commit_write()
}
});
return release_push_requests();
release_push_requests();
return true;
}
void multiplexer::add(std::shared_ptr<elem> const& info)
{
BOOST_ASSERT(!info->is_abandoned());
reqs_.push_back(info);
if (info->get_request().has_hello_priority()) {
if (request_access::has_priority(info->get_request())) {
auto rend = std::partition_point(std::rbegin(reqs_), std::rend(reqs_), [](auto const& e) {
return e->is_waiting();
});
@@ -81,7 +110,7 @@ void multiplexer::add(std::shared_ptr<elem> const& info)
}
}
std::pair<tribool, std::size_t> multiplexer::consume_next(system::error_code& ec)
consume_result multiplexer::consume_impl(system::error_code& ec)
{
// We arrive here in two states:
//
@@ -91,36 +120,34 @@ std::pair<tribool, std::size_t> multiplexer::consume_next(system::error_code& ec
// until the parsing of a complete message ends.
//
// 2. On a new message, in which case we have to determine
// whether the next messag is a push or a response.
// whether the next message is a push or a response.
//
auto const data = read_buffer_.get_commited();
BOOST_ASSERT(!data.empty());
if (!on_push_) // Prepare for new message.
on_push_ = is_next_push();
on_push_ = is_next_push(data);
if (on_push_) {
if (!resp3::parse(parser_, read_buffer_, receive_adapter_, ec))
return std::make_pair(std::nullopt, 0);
if (!resp3::parse(parser_, data, receive_adapter_, ec))
return consume_result::needs_more;
if (ec)
return std::make_pair(std::make_optional(true), 0);
auto const size = on_finish_parsing(true);
return std::make_pair(std::make_optional(true), size);
return consume_result::got_push;
}
BOOST_ASSERT_MSG(
is_waiting_response(),
"Not waiting for a response (using MONITOR command perhaps?)");
BOOST_ASSERT(!reqs_.empty());
BOOST_ASSERT(reqs_.front() != nullptr);
BOOST_ASSERT(reqs_.front()->get_remaining_responses() != 0);
BOOST_ASSERT(!reqs_.front()->is_waiting());
if (!resp3::parse(parser_, read_buffer_, reqs_.front()->get_adapter(), ec))
return std::make_pair(std::nullopt, 0);
if (!resp3::parse(parser_, data, reqs_.front()->get_adapter(), ec))
return consume_result::needs_more;
if (ec) {
reqs_.front()->notify_error(ec);
reqs_.pop_front();
return std::make_pair(std::make_optional(false), 0);
return consume_result::got_response;
}
reqs_.front()->commit_response(parser_.get_consumed());
@@ -130,14 +157,48 @@ std::pair<tribool, std::size_t> multiplexer::consume_next(system::error_code& ec
reqs_.pop_front();
}
auto const size = on_finish_parsing(false);
return std::make_pair(std::make_optional(false), size);
return consume_result::got_response;
}
std::pair<consume_result, std::size_t> multiplexer::consume(system::error_code& ec)
{
BOOST_ASSERT(!cancel_run_called_);
auto const ret = consume_impl(ec);
auto const consumed = parser_.get_consumed();
if (ec) {
return std::make_pair(ret, consumed);
}
if (ret != consume_result::needs_more) {
parser_.reset();
auto const res = read_buffer_.consume(consumed);
commit_usage(ret == consume_result::got_push, res);
return std::make_pair(ret, res.consumed);
}
return std::make_pair(consume_result::needs_more, consumed);
}
auto multiplexer::prepare_read() noexcept -> system::error_code { return read_buffer_.prepare(); }
auto multiplexer::get_prepared_read_buffer() noexcept -> read_buffer::span_type
{
return read_buffer_.get_prepared();
}
void multiplexer::commit_read(std::size_t bytes_read) { read_buffer_.commit(bytes_read); }
auto multiplexer::get_read_buffer_size() const noexcept -> std::size_t
{
return read_buffer_.get_commited().size();
}
void multiplexer::reset()
{
write_buffer_.clear();
read_buffer_.clear();
write_buffer_.clear();
write_offset_ = 0u;
parser_.reset();
on_push_ = false;
cancel_run_called_ = false;
@@ -145,6 +206,8 @@ void multiplexer::reset()
std::size_t multiplexer::prepare_write()
{
BOOST_ASSERT(!cancel_run_called_);
// Coalesces the requests and marks them staged. After a
// successful write staged requests will be marked as written.
auto const point = std::partition_point(
@@ -154,14 +217,15 @@ std::size_t multiplexer::prepare_write()
return !ri->is_waiting();
});
std::for_each(point, std::cend(reqs_), [this](auto const& ri) {
std::for_each(point, std::cend(reqs_), [this](const std::shared_ptr<elem>& ri) {
// Stage the request.
BOOST_ASSERT(!ri->is_abandoned());
write_buffer_ += ri->get_request().payload();
ri->mark_staged();
usage_.commands_sent += ri->get_request().get_commands();
});
usage_.bytes_sent += std::size(write_buffer_);
write_offset_ = 0u;
auto const d = std::distance(point, std::cend(reqs_));
return static_cast<std::size_t>(d);
@@ -186,18 +250,22 @@ std::size_t multiplexer::cancel_waiting()
return ret;
}
auto multiplexer::cancel_on_conn_lost() -> std::size_t
void multiplexer::cancel_on_conn_lost()
{
// Protects the code below from being called more than
// once, see https://github.com/boostorg/redis/issues/181
if (std::exchange(cancel_run_called_, true)) {
return 0;
}
// Should only be called once per reconnection.
// See https://github.com/boostorg/redis/issues/181
BOOST_ASSERT(!cancel_run_called_);
cancel_run_called_ = true;
// Must return false if the request should be removed.
auto cond = [](auto const& ptr) {
auto cond = [](const std::shared_ptr<elem>& ptr) {
BOOST_ASSERT(ptr != nullptr);
// Abandoned requests only make sense because a response for them might arrive.
// They should be discarded after the connection is lost
if (ptr->is_abandoned())
return false;
if (ptr->is_waiting()) {
return !ptr->get_request().get_config().cancel_on_connection_lost;
} else {
@@ -207,8 +275,6 @@ auto multiplexer::cancel_on_conn_lost() -> std::size_t
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond);
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});
});
@@ -218,39 +284,33 @@ auto multiplexer::cancel_on_conn_lost() -> std::size_t
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return ptr->mark_waiting();
});
return ret;
}
std::size_t multiplexer::on_finish_parsing(bool is_push)
void multiplexer::commit_usage(bool is_push, read_buffer::consume_result res)
{
if (is_push) {
usage_.pushes_received += 1;
usage_.push_bytes_received += parser_.get_consumed();
usage_.push_bytes_received += res.consumed;
on_push_ = false;
} else {
usage_.responses_received += 1;
usage_.response_bytes_received += parser_.get_consumed();
usage_.response_bytes_received += res.consumed;
}
on_push_ = false;
read_buffer_.erase(0, parser_.get_consumed());
auto const size = parser_.get_consumed();
parser_.reset();
return size;
usage_.bytes_rotated += res.rotated;
}
bool multiplexer::is_next_push() const noexcept
bool multiplexer::is_next_push(std::string_view data) const noexcept
{
BOOST_ASSERT(!read_buffer_.empty());
// Useful links to understand the heuristics below.
//
// - https://github.com/redis/redis/issues/11784
// - https://github.com/redis/redis/issues/6426
// - https://github.com/boostorg/redis/issues/170
// The message's resp3 type is a push.
if (resp3::to_type(read_buffer_.front()) == resp3::type::push)
// Test if the message resp3 type is a push.
BOOST_ASSERT(!data.empty());
if (resp3::to_type(data.front()) == resp3::type::push)
return true;
// This is non-push type and the requests queue is empty. I have
@@ -271,42 +331,38 @@ bool multiplexer::is_next_push() const noexcept
// Added to deal with MONITOR and also to fix PR170 which
// happens under load and on low-latency networks, where we
// might start receiving responses before the write operation
// completed and the request is still maked as staged and not
// completed and the request is still marked as staged and not
// written.
return reqs_.front()->is_waiting();
}
std::size_t multiplexer::release_push_requests()
void multiplexer::release_push_requests()
{
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return !(ptr->is_written() && ptr->get_request().get_expected_responses() == 0);
});
auto point = std::stable_partition(
std::begin(reqs_),
std::end(reqs_),
[](const std::shared_ptr<elem>& ptr) {
return !(ptr->is_written() && ptr->get_remaining_responses() == 0u);
});
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->notify_done();
});
auto const d = std::distance(point, std::end(reqs_));
reqs_.erase(point, std::end(reqs_));
return static_cast<std::size_t>(d);
}
bool multiplexer::is_waiting_response() const noexcept
void multiplexer::set_receive_adapter(any_adapter adapter)
{
if (std::empty(reqs_))
return false;
// Under load and on low-latency networks we might start
// receiving responses before the write operation completed and
// the request is still maked as staged and not written. See
// https://github.com/boostorg/redis/issues/170
return !reqs_.front()->is_waiting();
receive_adapter_ = std::move(adapter);
}
bool multiplexer::is_writing() const noexcept { return !write_buffer_.empty(); }
void multiplexer::set_config(config const& cfg)
{
read_buffer_.set_config({cfg.read_buffer_append_size, cfg.max_read_size});
}
auto make_elem(request const& req, multiplexer::pipeline_adapter_type adapter)
-> std::shared_ptr<multiplexer::elem>
auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>
{
return std::make_shared<multiplexer::elem>(req, std::move(adapter));
}

View File

@@ -0,0 +1,80 @@
/* 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 <boost/redis/detail/read_buffer.hpp>
#include <boost/assert.hpp>
#include <boost/core/make_span.hpp>
#include <utility>
namespace boost::redis::detail {
system::error_code read_buffer::prepare()
{
BOOST_ASSERT(append_buf_begin_ == buffer_.size());
auto const new_size = append_buf_begin_ + cfg_.read_buffer_append_size;
if (new_size > cfg_.max_read_size) {
return error::exceeds_maximum_read_buffer_size;
}
buffer_.resize(new_size);
return {};
}
void read_buffer::commit(std::size_t read_size)
{
BOOST_ASSERT(buffer_.size() >= (append_buf_begin_ + read_size));
buffer_.resize(append_buf_begin_ + read_size);
append_buf_begin_ = buffer_.size();
}
auto read_buffer::get_prepared() noexcept -> span_type
{
auto const size = buffer_.size();
return make_span(buffer_.data() + append_buf_begin_, size - append_buf_begin_);
}
auto read_buffer::get_commited() const noexcept -> std::string_view
{
return {buffer_.data(), append_buf_begin_};
}
void read_buffer::clear()
{
buffer_.clear();
append_buf_begin_ = 0;
}
read_buffer::consume_result
read_buffer::consume(std::size_t size)
{
// For convenience, if the requested size is larger than the
// committed buffer we cap it to the maximum.
if (size > append_buf_begin_)
size = append_buf_begin_;
buffer_.erase(buffer_.begin(), buffer_.begin() + size);
auto const rotated = size == 0u ? 0u : buffer_.size();
BOOST_ASSERT(append_buf_begin_ >= size);
append_buf_begin_ -= size;
return {size, rotated};
}
void read_buffer::reserve(std::size_t n) { buffer_.reserve(n); }
bool operator==(read_buffer const& lhs, read_buffer const& rhs)
{
return lhs.buffer_ == rhs.buffer_ && lhs.append_buf_begin_ == rhs.append_buf_begin_;
}
bool operator!=(read_buffer const& lhs, read_buffer const& rhs) { return !(lhs == rhs); }
} // namespace boost::redis::detail

View File

@@ -4,56 +4,100 @@
* accompanying file LICENSE.txt)
*/
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
namespace boost::redis::detail {
reader_fsm::reader_fsm(multiplexer& mpx) noexcept
: mpx_{&mpx}
{ }
reader_fsm::action reader_fsm::resume(
connection_state& st,
std::size_t bytes_read,
system::error_code ec,
asio::cancellation_type_t /*cancel_state*/)
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
BOOST_REDIS_YIELD(resume_point_, 1, action::type::setup_cancellation)
for (;;) {
BOOST_REDIS_YIELD(resume_point_, 2, next_read_type_)
// Prepare the buffer for the read operation
ec = st.mpx.prepare_read();
if (ec) {
log_debug(st.logger, "Reader task: error in prepare_read: ", ec);
return {ec};
}
// Read. The connection might spend health_check_interval without writing data.
// Give it another health_check_interval for the response to arrive.
// If we don't get anything in this time, consider the connection as dead
log_debug(st.logger, "Reader task: issuing read");
BOOST_REDIS_YIELD(resume_point_, 1, action::read_some(2 * st.cfg.health_check_interval))
// 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);
}
// Translate timeout errors caused by operation_aborted 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) {
ec = error::pong_timeout;
}
// Log what we read
if (ec) {
log_debug(st.logger, "Reader task: ", bytes_read, " bytes read, error: ", ec);
} else {
log_debug(st.logger, "Reader task: ", bytes_read, " bytes read");
}
// Process the bytes read, even if there was an error
st.mpx.commit_read(bytes_read);
// Check for read errors
if (ec) {
// TODO: If an error occurred but data was read (i.e.
// bytes_read != 0) we should try to process that data and
// deliver it to the user before calling cancel_run.
action_after_resume_ = {action::type::done, bytes_read, ec};
BOOST_REDIS_YIELD(resume_point_, 3, action::type::cancel_run)
return action_after_resume_;
return ec;
}
next_read_type_ = action::type::append_some;
while (!mpx_->get_read_buffer().empty()) {
res_ = mpx_->consume_next(ec);
// Process the data that we've read
while (st.mpx.get_read_buffer_size() != 0) {
res_ = st.mpx.consume(ec);
if (ec) {
action_after_resume_ = {action::type::done, res_.second, ec};
BOOST_REDIS_YIELD(resume_point_, 4, action::type::cancel_run)
return action_after_resume_;
// TODO: Perhaps log what has not been consumed to aid
// debugging.
log_debug(st.logger, "Reader task: error processing message: ", ec);
return ec;
}
if (!res_.first.has_value()) {
next_read_type_ = action::type::needs_more;
if (res_.first == consume_result::needs_more) {
log_debug(st.logger, "Reader task: incomplete message received");
break;
}
if (res_.first.value()) {
BOOST_REDIS_YIELD(resume_point_, 6, action::type::notify_push_receiver, res_.second)
if (res_.first == consume_result::got_push) {
BOOST_REDIS_YIELD(resume_point_, 2, action::notify_push_receiver(res_.second))
// 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);
}
// Check for other errors
if (ec) {
action_after_resume_ = {action::type::done, 0u, ec};
BOOST_REDIS_YIELD(resume_point_, 7, action::type::cancel_run)
return action_after_resume_;
log_debug(st.logger, "Reader task: error notifying push receiver: ", ec);
return ec;
}
} else {
// TODO: Here we should notify the exec operation that
@@ -68,7 +112,7 @@ reader_fsm::action reader_fsm::resume(
}
BOOST_ASSERT(false);
return {action::type::done, 0, system::error_code()};
return system::error_code();
}
} // namespace boost::redis::detail

View File

@@ -18,7 +18,16 @@ auto has_response(std::string_view cmd) -> bool
return true;
if (cmd == "UNSUBSCRIBE")
return true;
if (cmd == "PUNSUBSCRIBE")
return true;
return false;
}
request make_hello_request()
{
request req;
req.push("HELLO", "3");
return req;
}
} // namespace boost::redis::detail

View File

@@ -1,26 +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)
*/
#include <boost/redis/detail/resp3_handshaker.hpp>
namespace boost::redis::detail {
void push_hello(config const& cfg, request& req)
{
if (!cfg.username.empty() && !cfg.password.empty() && !cfg.clientname.empty())
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password, "SETNAME", cfg.clientname);
else if (cfg.password.empty() && cfg.clientname.empty())
req.push("HELLO", "3");
else if (cfg.clientname.empty())
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password);
else
req.push("HELLO", "3", "SETNAME", cfg.clientname);
if (cfg.database_index && cfg.database_index.value() != 0)
req.push("SELECT", cfg.database_index.value());
}
} // namespace boost::redis::detail

View File

@@ -0,0 +1,177 @@
//
// 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 <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/run_fsm.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/redis/impl/setup_request_utils.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/local/basic_endpoint.hpp> // for BOOST_ASIO_HAS_LOCAL_SOCKETS
#include <boost/system/error_code.hpp>
namespace boost::redis::detail {
inline system::error_code check_config(const config& cfg)
{
if (!cfg.unix_socket.empty()) {
if (cfg.use_ssl)
return error::unix_sockets_ssl_unsupported;
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
return error::unix_sockets_unsupported;
#endif
}
return system::error_code{};
}
inline void compose_ping_request(const config& cfg, request& to)
{
to.clear();
to.push("PING", cfg.health_check_id);
}
inline void process_setup_node(
connection_state& st,
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error:
case resp3::type::blob_error:
case resp3::type::null:
ec = redis::error::resp3_hello;
st.setup_diagnostic = nd.value;
break;
default:;
}
}
inline any_adapter make_setup_adapter(connection_state& st)
{
return any_adapter{
[&st](any_adapter::parse_event evt, resp3::node_view const& nd, system::error_code& ec) {
if (evt == any_adapter::parse_event::node)
process_setup_node(st, nd, ec);
}};
}
inline void on_setup_done(const multiplexer::elem& elm, connection_state& st)
{
const auto ec = elm.get_error();
if (ec) {
if (st.setup_diagnostic.empty()) {
log_info(st.logger, "Setup request execution: ", ec);
} else {
log_info(st.logger, "Setup request execution: ", ec, " (", st.setup_diagnostic, ")");
}
} else {
log_info(st.logger, "Setup request execution: success");
}
}
run_action run_fsm::resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Check config
ec = check_config(st.cfg);
if (ec) {
log_err(st.logger, "Invalid configuration: ", ec);
stored_ec_ = ec;
BOOST_REDIS_YIELD(resume_point_, 1, run_action_type::immediate)
return stored_ec_;
}
// Compose the setup request. This only depends on the config, so it can be done just once
compose_setup_request(st.cfg);
// Compose the PING request. Same as above
compose_ping_request(st.cfg, st.ping_req);
for (;;) {
// Try to connect
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::connect)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Run: cancelled (1)");
return system::error_code(asio::error::operation_aborted);
}
// If we were successful, run all the connection tasks
if (!ec) {
// Initialization
st.mpx.reset();
st.setup_diagnostic.clear();
// Add the setup request to the multiplexer
if (st.cfg.setup.get_commands() != 0u) {
auto elm = make_elem(st.cfg.setup, make_setup_adapter(st));
elm->set_done_callback([&elem_ref = *elm, &st] {
on_setup_done(elem_ref, st);
});
st.mpx.add(elm);
}
// Run the tasks
BOOST_REDIS_YIELD(resume_point_, 3, run_action_type::parallel_group)
// Store any error yielded by the tasks for later
stored_ec_ = ec;
// We've lost connection or otherwise been cancelled.
// Remove from the multiplexer the required requests.
st.mpx.cancel_on_conn_lost();
// The receive operation must be cancelled because channel
// subscription does not survive a reconnection but requires
// re-subscription.
BOOST_REDIS_YIELD(resume_point_, 4, run_action_type::cancel_receive)
// Restore the error
ec = stored_ec_;
}
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Run: cancelled (2)");
return system::error_code(asio::error::operation_aborted);
}
// If we are not going to try again, we're done
if (st.cfg.reconnect_wait_interval.count() == 0) {
return ec;
}
// Wait for the reconnection interval
BOOST_REDIS_YIELD(resume_point_, 5, run_action_type::wait_for_reconnection)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Run: cancelled (3)");
return system::error_code(asio::error::operation_aborted);
}
}
}
// We should never get here
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail

View File

@@ -0,0 +1,59 @@
/* 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_SETUP_REQUEST_UTILS_HPP
#define BOOST_REDIS_SETUP_REQUEST_UTILS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
namespace boost::redis::detail {
// Modifies config::setup to make a request suitable to be sent
// to the server using async_exec
inline void compose_setup_request(config& cfg)
{
if (!cfg.use_setup) {
// We're not using the setup request as-is, but should compose one based on
// the values passed by the user
auto& req = cfg.setup;
req.clear();
// Which parts of the command should we send?
// Don't send AUTH if the user is the default and the password is empty.
// Other users may have empty passwords.
// Note that this is just an optimization.
bool send_auth = !(
cfg.username.empty() || (cfg.username == "default" && cfg.password.empty()));
bool send_setname = !cfg.clientname.empty();
// Gather everything we can in a HELLO command
if (send_auth && send_setname)
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password, "SETNAME", cfg.clientname);
else if (send_auth)
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password);
else if (send_setname)
req.push("HELLO", "3", "SETNAME", cfg.clientname);
else
req.push("HELLO", "3");
// SELECT is independent of HELLO
if (cfg.database_index && cfg.database_index.value() != 0)
req.push("SELECT", cfg.database_index.value());
}
// In any case, the setup request should have the priority
// flag set so it's executed before any other request.
// The setup request should never be retried.
request_access::set_priority(cfg.setup, true);
cfg.setup.get_config().cancel_if_unresponded = true;
cfg.setup.get_config().cancel_on_connection_lost = true;
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_RUNNER_HPP

View File

@@ -0,0 +1,128 @@
//
// 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_WRITER_FSM_IPP
#define BOOST_REDIS_WRITER_FSM_IPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/writer_fsm.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
namespace boost::redis::detail {
inline void process_ping_node(
buffered_logger& lgr,
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
default: ;
}
if (ec) {
log_info(lgr, "Health checker: server answered ping with an error: ", nd.value);
}
}
inline any_adapter make_ping_adapter(buffered_logger& lgr)
{
return any_adapter{
[&lgr](any_adapter::parse_event evt, resp3::node_view const& nd, system::error_code& ec) {
if (evt == any_adapter::parse_event::node)
process_ping_node(lgr, nd, ec);
}};
}
writer_action writer_fsm::resume(
connection_state& st,
system::error_code ec,
std::size_t bytes_written,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
for (;;) {
// Attempt to write while we have requests ready to send
while (st.mpx.prepare_write() != 0u) {
// Write an entire message. We can't use asio::async_write because we want
// to apply timeouts to individual write operations
for (;;) {
// Write what we can. If nothing has been written for the health check
// interval, we consider the connection as failed
BOOST_REDIS_YIELD(
resume_point_,
1,
writer_action::write_some(st.cfg.health_check_interval))
// Commit the received bytes. This accounts for partial success
bool finished = st.mpx.commit_write(bytes_written);
log_debug(st.logger, "Writer task: ", bytes_written, " bytes written.");
// 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 = error::write_timeout;
// Check for errors
if (ec) {
if (ec == asio::error::operation_aborted) {
log_debug(st.logger, "Writer task: cancelled (1).");
} else {
log_debug(st.logger, "Writer task error: ", ec);
}
return ec;
}
// Are we done yet?
if (finished)
break;
}
}
// No more requests ready to be written. Wait for more, or until we need to send a PING
BOOST_REDIS_YIELD(resume_point_, 2, writer_action::wait(st.cfg.health_check_interval))
// 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);
}
// If we weren't notified, it's because there is no data and we should send a health check
if (!ec) {
auto elem = make_elem(st.ping_req, make_ping_adapter(st.logger));
elem->set_done_callback([] { });
st.mpx.add(elem);
}
}
}
// We should never reach here
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail
#endif

View File

@@ -8,6 +8,7 @@
#define BOOST_REDIS_LOGGER_HPP
#include <functional>
#include <string>
#include <string_view>
namespace boost::redis {
@@ -92,6 +93,15 @@ struct logger {
std::function<void(level, std::string_view)> fn;
};
namespace detail {
struct buffered_logger {
logger lgr;
std::string buffer{};
};
} // namespace detail
} // namespace boost::redis
#endif // BOOST_REDIS_LOGGER_HPP

View File

@@ -16,22 +16,68 @@ namespace boost::redis {
*/
enum class operation
{
/// Resolve operation.
/**
* @brief (Deprecated) Resolve operation.
*
* Cancelling a single resolve operation is probably not what you
* want, since there is no way to detect when a connection is performing name resolution.
* Use @ref operation::run to cancel the current @ref basic_connection::async_run operation,
* which includes name resolution.
*/
resolve,
/// Connect operation.
/**
* @brief (Deprecated) Connect operation.
*
* Cancelling a single connect operation is probably not what you
* want, since there is no way to detect when a connection is performing a connect operation.
* Use @ref operation::run to cancel the current @ref basic_connection::async_run operation,
* which includes connection establishment.
*/
connect,
/// SSL handshake operation.
/**
* @brief (Deprecated) SSL handshake operation.
*
* Cancelling a single connect operation is probably not what you
* want, since there is no way to detect when a connection is performing an SSL handshake.
* Use @ref operation::run to cancel the current @ref basic_connection::async_run operation,
* which includes the SSL handshake.
*/
ssl_handshake,
/// Refers to `connection::async_exec` operations.
exec,
/// Refers to `connection::async_run` operations.
run,
/// Refers to `connection::async_receive` operations.
receive,
/// Cancels reconnection.
/**
* @brief (Deprecated) Cancels reconnection.
*
* Cancelling reconnection doesn't really cancel anything.
* It will only prevent further connections attempts from being
* made once the current connection encounters an error.
*
* Use @ref operation::run to cancel the current @ref basic_connection::async_run operation,
* which includes reconnection. If you want to disable reconnection completely,
* set @ref config::reconnect_wait_interval to zero before calling `async_run`.
*/
reconnection,
/// Health check operation.
/**
* @brief (Deprecated) Health check operation.
*
* Cancelling the health checker only is probably not what you want.
* Use @ref operation::run to cancel the current @ref basic_connection::async_run operation,
* which includes the health checker. If you want to disable health checks completely,
* set @ref config::health_check_interval to zero before calling `async_run`.
*/
health_check,
/// Refers to all operations.
all,
};

View File

@@ -21,7 +21,8 @@ namespace boost::redis {
namespace detail {
auto has_response(std::string_view cmd) -> bool;
}
struct request_access;
} // namespace detail
/** @brief Represents a Redis request.
*
@@ -46,31 +47,49 @@ class request {
public:
/// Request configuration options.
struct config {
/** @brief If `true`, calls to @ref basic_connection::async_exec will
/** @brief (Deprecated) If `true`, calls to @ref basic_connection::async_exec will
* complete with error if the connection is lost while the
* request hasn't been sent yet.
*
* @par Deprecated
* This setting is deprecated and should be always left out as the default
* (waiting for a connection to be established again).
* If you need to limit how much time a @ref basic_connection::async_exec
* operation is allowed to take, use `asio::cancel_after`, instead.
*/
bool cancel_on_connection_lost = true;
bool cancel_on_connection_lost = false;
/** @brief If `true`, @ref basic_connection::async_exec will complete with
/** @brief (Deprecated) If `true`, @ref basic_connection::async_exec will complete with
* @ref boost::redis::error::not_connected if the call happens
* before the connection with Redis was established.
*
* @par Deprecated
* This setting is deprecated and should be always left out as the default
* (waiting for a connection to be established).
* If you need to limit how much time a @ref basic_connection::async_exec
* operation is allowed to take, use `asio::cancel_after`, instead.
*/
bool cancel_if_not_connected = false;
/** @brief If `false`, @ref basic_connection::async_exec will not
* automatically cancel this request if the connection is lost.
* Affects only requests that have been written to the socket
* Affects only requests that have been written to the server
* but have not been responded when
* @ref boost::redis::connection::async_run completes.
* the connection is lost.
*/
bool cancel_if_unresponded = true;
/** @brief If this request has a `HELLO` command and this flag
/** @brief (Deprecated) If this request has a `HELLO` command and this flag
* is `true`, it will be moved to the
* front of the queue of awaiting requests. This makes it
* possible to send `HELLO` commands and authenticate before other
* commands are sent.
*
* @par Deprecated
* This field has been superseded by @ref config::setup.
* This setup request will always be run first on connection establishment.
* Please use it to run any required setup commands.
* This field will be removed in subsequent releases.
*/
bool hello_with_priority = true;
};
@@ -79,7 +98,7 @@ public:
*
* @param cfg Configuration options.
*/
explicit request(config cfg = config{true, false, true, true})
explicit request(config cfg = config{false, false, true, true})
: cfg_{cfg}
{ }
@@ -94,7 +113,11 @@ public:
[[nodiscard]] auto payload() const noexcept -> std::string_view { return payload_; }
[[nodiscard]] auto has_hello_priority() const noexcept -> auto const&
[[nodiscard]]
BOOST_DEPRECATED(
"The hello_with_priority attribute and related functions are deprecated. "
"Use config::setup to run setup commands, instead.") auto has_hello_priority() const noexcept
-> auto const&
{
return has_hello_priority_;
}
@@ -332,8 +355,22 @@ private:
std::size_t commands_ = 0;
std::size_t expected_responses_ = 0;
bool has_hello_priority_ = false;
friend struct detail::request_access;
};
namespace detail {
struct request_access {
inline static void set_priority(request& r, bool value) { r.has_hello_priority_ = value; }
inline static bool has_priority(const request& r) { return r.has_hello_priority_; }
};
// Creates a HELLO 3 request
request make_hello_request();
} // namespace detail
} // namespace boost::redis
#endif // BOOST_REDIS_REQUEST_HPP

View File

@@ -27,22 +27,10 @@ parser::parser() { reset(); }
void parser::reset()
{
depth_ = 0;
sizes_ = {{1}};
bulk_length_ = (std::numeric_limits<std::size_t>::max)();
sizes_ = default_sizes;
bulk_length_ = default_bulk_length;
bulk_ = type::invalid;
consumed_ = 0;
sizes_[0] = 2; // The sentinel must be more than 1.
}
std::size_t parser::get_suggested_buffer_growth(std::size_t hint) const noexcept
{
if (!bulk_expected())
return hint;
if (hint < bulk_length_ + 2)
return bulk_length_ + 2;
return hint;
}
std::size_t parser::get_consumed() const noexcept { return consumed_; }
@@ -206,4 +194,13 @@ auto parser::consume_impl(type t, std::string_view elem, system::error_code& ec)
return ret;
}
bool parser::is_parsing() const noexcept
{
auto const v = depth_ == 0 && sizes_ == default_sizes && bulk_length_ == default_bulk_length &&
bulk_ == type::invalid && consumed_ == 0;
return !v;
}
} // namespace boost::redis::resp3

View File

@@ -27,6 +27,14 @@ public:
static constexpr std::string_view sep = "\r\n";
private:
using sizes_type = std::array<std::size_t, max_embedded_depth + 1>;
// sizes_[0] = 2 because the sentinel must be more than 1.
static constexpr sizes_type default_sizes = {
{2, 1, 1, 1, 1, 1}
};
static constexpr auto default_bulk_length = static_cast<std::size_t>(-1);
// The current depth. Simple data types will have depth 0, whereas
// the elements of aggregates will have depth 1. Embedded types
// will have increasing depth.
@@ -35,7 +43,7 @@ private:
// The parser supports up to 5 levels of nested structures. The
// first element in the sizes stack is a sentinel and must be
// different from 1.
std::array<std::size_t, max_embedded_depth + 1> sizes_;
sizes_type sizes_;
// Contains the length expected in the next bulk read.
std::size_t bulk_length_;
@@ -67,21 +75,26 @@ public:
[[nodiscard]]
auto done() const noexcept -> bool;
auto get_suggested_buffer_growth(std::size_t hint) const noexcept -> std::size_t;
auto get_consumed() const noexcept -> std::size_t;
auto consume(std::string_view view, system::error_code& ec) noexcept -> result;
void reset();
bool is_parsing() const noexcept;
};
// Returns false if more data is needed. If true is returned the
// parser is either done or an error occured, that can be checked on
// ec.
template <class Adapter>
bool parse(resp3::parser& p, std::string_view const& msg, Adapter& adapter, system::error_code& ec)
bool parse(parser& p, std::string_view const& msg, Adapter& adapter, system::error_code& ec)
{
// This if could be avoid with a state machine that jumps into the
// correct position.
if (!p.is_parsing())
adapter.on_init();
while (!p.done()) {
auto const res = p.consume(msg, ec);
if (ec)
@@ -90,11 +103,12 @@ bool parse(resp3::parser& p, std::string_view const& msg, Adapter& adapter, syst
if (!res)
return false;
adapter(res.value(), ec);
adapter.on_node(res.value(), ec);
if (ec)
return true;
}
adapter.on_done();
return true;
}

View File

@@ -108,6 +108,8 @@ namespace detail {
template <class Adapter>
void deserialize(std::string_view const& data, Adapter adapter, system::error_code& ec)
{
adapter.on_init();
parser parser;
while (!parser.done()) {
auto const res = parser.consume(data, ec);
@@ -116,12 +118,14 @@ void deserialize(std::string_view const& data, Adapter adapter, system::error_co
BOOST_ASSERT(res.has_value());
adapter(res.value(), ec);
adapter.on_node(res.value(), ec);
if (ec)
return;
}
BOOST_ASSERT(parser.get_consumed() == std::size(data));
adapter.on_done();
}
template <class Adapter>

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -10,7 +10,7 @@
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/system.hpp>
#include <boost/system/error_code.hpp>
#include <string>
#include <tuple>

View File

@@ -4,17 +4,19 @@
* accompanying file LICENSE.txt)
*/
#include <boost/redis/impl/connect_fsm.ipp>
#include <boost/redis/impl/connection.ipp>
#include <boost/redis/impl/connection_logger.ipp>
#include <boost/redis/impl/error.ipp>
#include <boost/redis/impl/exec_fsm.ipp>
#include <boost/redis/impl/ignore.ipp>
#include <boost/redis/impl/logger.ipp>
#include <boost/redis/impl/multiplexer.ipp>
#include <boost/redis/impl/read_buffer.ipp>
#include <boost/redis/impl/reader_fsm.ipp>
#include <boost/redis/impl/request.ipp>
#include <boost/redis/impl/resp3_handshaker.ipp>
#include <boost/redis/impl/response.ipp>
#include <boost/redis/impl/run_fsm.ipp>
#include <boost/redis/impl/writer_fsm.ipp>
#include <boost/redis/resp3/impl/parser.ipp>
#include <boost/redis/resp3/impl/serialization.ipp>
#include <boost/redis/resp3/impl/type.ipp>

View File

@@ -36,6 +36,9 @@ struct usage {
/// Number of push-bytes received.
std::size_t push_bytes_received = 0;
/// Number of bytes rotated in the read buffer.
std::size_t bytes_rotated = 0;
};
} // namespace boost::redis

View File

@@ -15,7 +15,7 @@ target_compile_features(boost_redis_src PRIVATE cxx_std_17)
target_link_libraries(boost_redis_src PRIVATE boost_redis_project_options)
# Test utils
add_library(boost_redis_tests_common STATIC common.cpp)
add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp)
target_compile_features(boost_redis_tests_common PRIVATE cxx_std_17)
target_link_libraries(boost_redis_tests_common PRIVATE boost_redis_project_options)
@@ -40,25 +40,33 @@ make_test(test_any_adapter)
make_test(test_exec_fsm)
make_test(test_log_to_file)
make_test(test_conn_logging)
make_test(test_writer_fsm)
make_test(test_reader_fsm)
make_test(test_connect_fsm)
make_test(test_run_fsm)
make_test(test_setup_request_utils)
make_test(test_multiplexer)
# Tests that require a real Redis server
make_test(test_conn_quit)
make_test(test_conn_tls)
make_test(test_conn_exec_retry)
make_test(test_conn_exec_error)
make_test(test_run)
make_test(test_conn_run_cancel)
make_test(test_conn_check_health)
make_test(test_conn_exec)
make_test(test_conn_push)
make_test(test_conn_monitor)
make_test(test_conn_reconnect)
make_test(test_conn_exec_cancel)
make_test(test_conn_exec_cancel2)
make_test(test_conn_echo_stress)
make_test(test_conn_move)
make_test(test_conn_setup)
make_test(test_issue_50)
make_test(test_issue_181)
make_test(test_conversions)
make_test(test_conn_tls)
make_test(test_unix_sockets)
make_test(test_conn_cancel_after)
# Coverage
set(

View File

@@ -42,6 +42,7 @@ lib redis_test_common
:
boost_redis.cpp
common.cpp
sansio_utils.cpp
: requirements $(requirements)
: usage-requirements $(requirements)
;
@@ -56,7 +57,12 @@ local tests =
test_exec_fsm
test_log_to_file
test_conn_logging
test_writer_fsm
test_reader_fsm
test_run_fsm
test_connect_fsm
test_setup_request_utils
test_multiplexer
;
# Build and run the tests

View File

@@ -50,7 +50,6 @@ boost::redis::config make_test_config()
{
boost::redis::config cfg;
cfg.addr.host = get_server_hostname();
cfg.max_read_size = 1000000;
return cfg;
}
@@ -69,3 +68,19 @@ void run_coroutine_test(net::awaitable<void> op, std::chrono::steady_clock::dura
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
// TODO: duplicated
std::string_view find_client_info(std::string_view client_info, std::string_view key)
{
std::string prefix{key};
prefix += '=';
auto const pos = client_info.find(prefix);
if (pos == std::string_view::npos)
return {};
auto const pos_begin = pos + prefix.size();
auto const pos_end = client_info.find(' ', pos_begin);
return client_info.substr(pos_begin, pos_end - pos_begin);
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include <boost/redis/connection.hpp>
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/operation.hpp>
#include <boost/asio/awaitable.hpp>
@@ -10,6 +11,7 @@
#include <chrono>
#include <memory>
#include <string_view>
// The timeout for tests involving communication to a real server.
// Some tests use a longer timeout by multiplying this value by some
@@ -34,3 +36,7 @@ void run(
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);

75
test/sansio_utils.cpp Normal file
View File

@@ -0,0 +1,75 @@
/* 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 <boost/redis/detail/multiplexer.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/core/lightweight_test.hpp>
#include "sansio_utils.hpp"
#include <initializer_list>
#include <iostream>
#include <ostream>
using namespace boost::redis;
static constexpr const char* to_string(logger::level lvl)
{
switch (lvl) {
case logger::level::disabled: return "logger::level::disabled";
case logger::level::emerg: return "logger::level::emerg";
case logger::level::alert: return "logger::level::alert";
case logger::level::crit: return "logger::level::crit";
case logger::level::err: return "logger::level::err";
case logger::level::warning: return "logger::level::warning";
case logger::level::notice: return "logger::level::notice";
case logger::level::info: return "logger::level::info";
case logger::level::debug: return "logger::level::debug";
default: return "<unknown logger::level>";
}
}
namespace boost::redis::detail {
void read(multiplexer& mpx, std::string_view data)
{
auto const ec = mpx.prepare_read();
ignore_unused(ec);
BOOST_ASSERT(ec == system::error_code{});
auto const buffer = mpx.get_prepared_read_buffer();
BOOST_ASSERT(buffer.size() >= data.size());
std::copy(data.cbegin(), data.cend(), buffer.begin());
mpx.commit_read(data.size());
}
// Operators to enable checking logs
bool operator==(const log_message& lhs, const log_message& rhs) noexcept
{
return lhs.lvl == rhs.lvl && lhs.msg == rhs.msg;
}
std::ostream& operator<<(std::ostream& os, const log_message& v)
{
return os << "log_message { .lvl=" << to_string(v.lvl) << ", .msg=" << v.msg << " }";
}
void log_fixture::check_log(std::initializer_list<const log_message> expected, source_location loc)
const
{
if (!BOOST_TEST_ALL_EQ(expected.begin(), expected.end(), msgs.begin(), msgs.end())) {
std::cerr << "Called from " << loc << std::endl;
}
}
logger log_fixture::make_logger()
{
return logger(logger::level::debug, [&](logger::level lvl, std::string_view msg) {
msgs.push_back({lvl, std::string(msg)});
});
}
} // namespace boost::redis::detail

55
test/sansio_utils.hpp Normal file
View File

@@ -0,0 +1,55 @@
/* 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_TEST_SANSIO_UTILS_HPP
#define BOOST_REDIS_TEST_SANSIO_UTILS_HPP
#include <boost/redis/logger.hpp>
#include <boost/assert/source_location.hpp>
#include <chrono>
#include <initializer_list>
#include <string>
#include <string_view>
namespace boost::redis::detail {
class multiplexer;
// Read data into the multiplexer with the following steps
//
// 1. prepare_read
// 2. get_read_buffer
// 3. Copy data in the buffer from 2.
// 4. commit_read;
//
// This is used in the multiplexer tests.
void read(multiplexer& mpx, std::string_view data);
// Utilities for checking logs
struct log_message {
logger::level lvl;
std::string msg;
};
struct log_fixture {
std::vector<log_message> msgs;
void check_log(
std::initializer_list<const log_message> expected,
source_location loc = BOOST_CURRENT_LOCATION) const;
logger make_logger();
};
constexpr auto to_milliseconds(std::chrono::steady_clock::duration d)
{
return std::chrono::duration_cast<std::chrono::milliseconds>(d).count();
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -16,6 +16,7 @@ using boost::redis::generic_response;
using boost::redis::response;
using boost::redis::ignore;
using boost::redis::any_adapter;
using boost::redis::any_adapter;
BOOST_AUTO_TEST_CASE(any_adapter_response_types)
{
@@ -34,13 +35,13 @@ BOOST_AUTO_TEST_CASE(any_adapter_copy_move)
{
// any_adapter can be copied/moved
response<int, std::string> r;
any_adapter ad1{r};
auto ad1 = any_adapter{r};
// copy constructor
any_adapter ad2{ad1};
auto ad2 = any_adapter(ad1);
// move constructor
any_adapter ad3{std::move(ad2)};
auto ad3 = any_adapter(std::move(ad2));
// copy assignment
BOOST_CHECK_NO_THROW(ad2 = ad1);

View File

@@ -0,0 +1,110 @@
//
// 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 <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/experimental/channel_error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
using namespace std::chrono_literals;
namespace asio = boost::asio;
using boost::system::error_code;
using boost::redis::request;
using boost::redis::basic_connection;
using boost::redis::connection;
using boost::redis::ignore;
using boost::redis::generic_response;
namespace {
template <class Connection>
void test_run()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool run_finished = false;
// 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);
run_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
}
template <class Connection>
void test_exec()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool exec_finished = false;
request req;
req.push("PING", "cancel_after");
// 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);
exec_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
}
template <class Connection>
void test_receive()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool receive_finished = false;
generic_response resp;
conn.set_receive_response(resp);
// Call the function with a very short timeout.
conn.async_receive(asio::cancel_after(1ms, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, asio::experimental::channel_errc::channel_cancelled);
receive_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(receive_finished);
}
} // namespace
int main()
{
test_run<basic_connection<asio::io_context::executor_type>>();
test_run<connection>();
test_exec<basic_connection<asio::io_context::executor_type>>();
test_exec<connection>();
test_receive<basic_connection<asio::io_context::executor_type>>();
test_receive<connection>();
return boost::report_errors();
}

View File

@@ -5,16 +5,20 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <cstddef>
#define BOOST_TEST_MODULE check_health
#include <boost/test/included/unit_test.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
#include <iostream>
#include <thread>
#include <chrono>
#include <cstddef>
#include <string>
namespace net = boost::asio;
namespace redis = boost::redis;
@@ -22,116 +26,240 @@ using error_code = boost::system::error_code;
using connection = boost::redis::connection;
using boost::redis::request;
using boost::redis::ignore;
using boost::redis::operation;
using boost::redis::generic_response;
using boost::redis::consume_one;
using namespace std::chrono_literals;
// TODO: Test cancel(health_check)
namespace {
struct push_callback {
connection* conn1;
connection* conn2;
generic_response* resp2;
request* req1;
int i = 0;
boost::asio::coroutine coro{};
void operator()(error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER(coro) for (;;)
{
BOOST_ASIO_CORO_YIELD
conn2->async_receive(*this);
if (ec) {
std::clog << "Exiting." << std::endl;
return;
}
BOOST_TEST(resp2->has_value());
BOOST_TEST(!resp2->value().empty());
std::clog << "Event> " << resp2->value().front().value << std::endl;
consume_one(*resp2);
++i;
if (i == 5) {
std::clog << "Pausing the server" << std::endl;
// Pause the redis server to test if the health-check exits.
BOOST_ASIO_CORO_YIELD
conn1->async_exec(*req1, ignore, *this);
std::clog << "After pausing> " << ec.message() << std::endl;
// Don't know in CI we are getting: Got RESP3 simple-error.
//BOOST_TEST(!ec);
conn2->cancel(operation::run);
conn2->cancel(operation::receive);
conn2->cancel(operation::reconnection);
return;
}
}
};
};
BOOST_AUTO_TEST_CASE(check_health)
// The health checker detects dead connections and triggers reconnection
void test_reconnection()
{
// Setup
net::io_context ioc;
connection conn1{ioc};
connection conn{ioc};
// This request will block forever, causing the connection to become unresponsive
request req1;
req1.push("CLIENT", "PAUSE", "10000", "ALL");
auto cfg1 = make_test_config();
cfg1.health_check_id = "conn1";
cfg1.reconnect_wait_interval = std::chrono::seconds::zero();
bool run1_finished = false, run2_finished = false, exec_finished = false;
conn1.async_run(cfg1, {}, [&](error_code ec) {
run1_finished = true;
std::cout << "async_run 1 completed: " << ec.message() << std::endl;
BOOST_TEST(ec != error_code());
});
//--------------------------------
// It looks like client pause does not work for clients that are
// sending MONITOR. I will therefore open a second connection.
connection conn2{ioc};
auto cfg2 = make_test_config();
cfg2.health_check_id = "conn2";
conn2.async_run(cfg2, {}, [&](error_code ec) {
run2_finished = true;
std::cout << "async_run 2 completed: " << ec.message() << std::endl;
BOOST_TEST(ec != error_code());
});
req1.push("BLPOP", "any", 0);
// This request should be executed after reconnection
request req2;
req2.push("MONITOR");
generic_response resp2;
conn2.set_receive_response(resp2);
req2.push("PING", "after_reconnection");
req2.get_config().cancel_if_unresponded = false;
req2.get_config().cancel_on_connection_lost = false;
conn2.async_exec(req2, ignore, [&exec_finished](error_code ec, std::size_t) {
exec_finished = true;
std::cout << "async_exec: " << std::endl;
BOOST_TEST(ec == error_code());
// Make the test run faster
auto cfg = make_test_config();
cfg.health_check_interval = 500ms;
cfg.reconnect_wait_interval = 100ms;
bool run_finished = false, exec1_finished = false, exec2_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
//--------------------------------
// 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).
conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) {
exec1_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
push_callback{&conn1, &conn2, &resp2, &req1}(); // Starts reading pushes.
// Execute the second request. This one will succeed after reconnection
conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) {
exec2_finished = true;
BOOST_TEST_EQ(ec2, error_code());
conn.cancel();
});
});
ioc.run_for(2 * test_timeout);
ioc.run_for(test_timeout);
BOOST_TEST(run1_finished);
BOOST_TEST(run2_finished);
BOOST_TEST(exec_finished);
// Waits before exiting otherwise it might cause subsequent tests
// to fail.
std::this_thread::sleep_for(std::chrono::seconds{10});
BOOST_TEST(run_finished);
BOOST_TEST(exec1_finished);
BOOST_TEST(exec2_finished);
}
} // namespace
// We use the correct error code when a ping times out
void test_error_code()
{
// Setup
net::io_context ioc;
connection conn{ioc};
// This request will block forever, causing the connection to become unresponsive
request req;
req.push("BLPOP", "any", 0);
// Make the test run faster
auto cfg = make_test_config();
cfg.health_check_interval = 200ms;
cfg.reconnect_wait_interval = 0s;
bool run_finished = false, exec_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout);
});
// This request will complete after the health checker deems the connection
// as unresponsive and triggers a reconnection (it's configured to be cancelled
// if unresponded).
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
}
// A ping interval of zero disables timeouts (and doesn't cause trouble)
void test_disabled()
{
// Setup
net::io_context ioc;
connection conn{ioc};
// 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 cfg = make_test_config();
cfg.health_check_interval = 0s;
bool run_finished = false, exec1_finished = false, exec2_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) {
exec1_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) {
exec2_finished = true;
BOOST_TEST_EQ(ec2, error_code());
conn.cancel();
});
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec1_finished);
BOOST_TEST(exec2_finished);
}
// 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.
class test_flexible {
net::io_context ioc;
connection conn1{ioc}; // The one that simulates a heavy load condition
connection conn2{ioc}; // Publishes messages
net::steady_timer timer{ioc};
request publish_req;
bool run1_finished = false, run2_finished = false, exec_finished{false},
publisher_finished{false};
// Starts publishing messages to the channel
void start_publish()
{
conn2.async_exec(publish_req, ignore, [this](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
if (exec_finished) {
// The blocking request finished, we're done
conn2.cancel();
publisher_finished = true;
} else {
// Wait for some time and publish again
timer.expires_after(100ms);
timer.async_wait([this](error_code ec) {
BOOST_TEST_EQ(ec, error_code());
start_publish();
});
}
});
}
// Generates a sufficiently unique name for channels so
// tests may be run in parallel for different configurations
static 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());
}
public:
test_flexible() = default;
void run()
{
// Setup
auto cfg = make_test_config();
cfg.health_check_interval = 500ms;
generic_response resp;
std::string channel_name = make_unique_id();
publish_req.push("PUBLISH", channel_name, "test_health_check_flexible");
// 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;
conn1.async_run(cfg, [&](error_code ec) {
run1_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
conn2.async_run(cfg, [&](error_code ec) {
run2_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
// BLPOP will return NIL, so we can't use ignore
conn1.async_exec(blocking_req, resp, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn1.cancel();
});
start_publish();
ioc.run_for(test_timeout);
BOOST_TEST(run1_finished);
BOOST_TEST(run2_finished);
BOOST_TEST(exec_finished);
BOOST_TEST(publisher_finished);
}
};
} // namespace
int main()
{
test_reconnection();
test_error_code();
test_disabled();
test_flexible().run();
return boost::report_errors();
}

View File

@@ -43,8 +43,9 @@ std::ostream& operator<<(std::ostream& os, usage const& u)
<< "Bytes sent: " << u.bytes_sent << "\n"
<< "Responses received: " << u.responses_received << "\n"
<< "Pushes received: " << u.pushes_received << "\n"
<< "Response bytes received: " << u.response_bytes_received << "\n"
<< "Push bytes received: " << u.push_bytes_received;
<< "Bytes received (response): " << u.response_bytes_received << "\n"
<< "Bytes received (push): " << u.push_bytes_received << "\n"
<< "Bytes rotated: " << u.bytes_rotated;
return os;
}
@@ -94,7 +95,6 @@ BOOST_AUTO_TEST_CASE(echo_stress)
net::io_context ctx;
connection conn{ctx};
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds::zero();
// Number of coroutines that will send pings sharing the same
// connection to redis.

View File

@@ -31,6 +31,7 @@ using boost::redis::ignore;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::any_adapter;
using boost::system::error_code;
using namespace std::chrono_literals;
@@ -121,68 +122,6 @@ BOOST_AUTO_TEST_CASE(wrong_response_data_type)
BOOST_TEST(finished);
}
BOOST_AUTO_TEST_CASE(cancel_request_if_not_connected)
{
request req;
req.get_config().cancel_if_not_connected = true;
req.push("PING");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
bool finished = false;
conn->async_exec(req, ignore, [conn, &finished](error_code ec, std::size_t) {
BOOST_TEST(ec, boost::redis::error::not_connected);
conn->cancel();
finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(finished);
}
BOOST_AUTO_TEST_CASE(correct_database)
{
auto cfg = make_test_config();
cfg.database_index = 2;
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req;
req.push("CLIENT", "LIST");
generic_response resp;
bool exec_finished = false, run_finished = false;
conn->async_exec(req, resp, [&](error_code ec, std::size_t n) {
BOOST_TEST(ec == error_code());
std::clog << "async_exec has completed: " << n << std::endl;
conn->cancel();
exec_finished = true;
});
conn->async_run(cfg, {}, [&run_finished](error_code) {
std::clog << "async_run has exited." << std::endl;
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST_REQUIRE(exec_finished);
BOOST_TEST_REQUIRE(run_finished);
BOOST_TEST_REQUIRE(!resp.value().empty());
auto const& value = resp.value().front().value;
auto const pos = value.find("db=");
auto const index_str = value.substr(pos + 3, 1);
auto const index = std::stoi(index_str);
// This check might fail if more than one client is connected to
// redis when the CLIENT LIST command is run.
BOOST_CHECK_EQUAL(cfg.database_index.value(), index);
}
BOOST_AUTO_TEST_CASE(large_number_of_concurrent_requests_issue_170)
{
// See https://github.com/boostorg/redis/issues/170
@@ -195,8 +134,7 @@ BOOST_AUTO_TEST_CASE(large_number_of_concurrent_requests_issue_170)
auto conn = std::make_shared<connection>(ioc);
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds(0);
conn->async_run(cfg, {}, net::detached);
conn->async_run(cfg, net::detached);
constexpr int repeat = 8000;
int remaining = repeat;
@@ -229,7 +167,7 @@ BOOST_AUTO_TEST_CASE(exec_any_adapter)
bool finished = false;
conn->async_exec(req, boost::redis::any_adapter(res), [&](error_code ec, std::size_t) {
conn->async_exec(req, res, [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->cancel();
finished = true;
@@ -242,4 +180,4 @@ BOOST_AUTO_TEST_CASE(exec_any_adapter)
BOOST_TEST(std::get<0>(res).value() == "PONG");
}
} // namespace
} // namespace

View File

@@ -7,33 +7,27 @@
#include <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/errc.hpp>
#include <cstddef>
#define BOOST_TEST_MODULE conn_exec_cancel
#include <boost/asio/detached.hpp>
#include <boost/test/included/unit_test.hpp>
#include "common.hpp"
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <cstddef>
#include <iostream>
using namespace std::chrono_literals;
// NOTE1: I have observed that if hello and
// blpop are sent together, Redis will send the response of hello
// right away, not waiting for blpop.
namespace net = boost::asio;
using error_code = boost::system::error_code;
using namespace net::experimental::awaitable_operators;
using boost::redis::operation;
using boost::redis::error;
using boost::redis::request;
@@ -47,93 +41,9 @@ using namespace std::chrono_literals;
namespace {
auto implicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds::zero();
run(conn, cfg);
// See NOTE1.
request req0;
req0.push("PING");
co_await conn->async_exec(req0, ignore);
// Will be cancelled after it has been written but before the
// response arrives.
request req1;
req1.push("BLPOP", "any", 3);
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
// Achieves implicit cancellation when the timer fires.
boost::system::error_code ec1, ec2;
co_await (conn->async_exec(req1, ignore, redir(ec1)) || st.async_wait(redir(ec2)));
conn->cancel();
// I have observed this produces terminal cancellation so it can't
// be ignored, an error is expected.
BOOST_TEST(ec1 == net::error::operation_aborted);
BOOST_TEST(ec2 == error_code());
}
BOOST_AUTO_TEST_CASE(test_ignore_implicit_cancel_of_req_written)
{
run_coroutine_test(implicit_cancel_of_req_written());
}
BOOST_AUTO_TEST_CASE(test_cancel_of_req_written_on_run_canceled)
{
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req0;
req0.push("PING");
// Sends a request that will be blocked forever, so we can test
// canceling it while waiting for a response.
request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.get_config().cancel_if_unresponded = true;
req1.push("BLPOP", "any", 0);
bool finished = false;
auto c1 = [&](error_code ec, std::size_t) {
BOOST_CHECK_EQUAL(ec, net::error::operation_aborted);
finished = true;
};
auto c0 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req1, ignore, c1);
};
conn->async_exec(req0, ignore, c0);
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds{5};
run(conn);
net::steady_timer st{ioc};
st.expires_after(std::chrono::seconds{1});
st.async_wait([&](error_code ec) {
BOOST_TEST(ec == error_code());
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
ioc.run_for(test_timeout);
BOOST_TEST(finished);
}
// We can cancel requests that haven't been written yet.
// All cancellation types are supported here.
BOOST_AUTO_TEST_CASE(test_cancel_pending)
void test_cancel_pending()
{
struct {
const char* name;
@@ -145,38 +55,247 @@ BOOST_AUTO_TEST_CASE(test_cancel_pending)
};
for (const auto& tc : test_cases) {
BOOST_TEST_CONTEXT(tc.name)
{
// Setup
net::io_context ctx;
connection conn(ctx);
request req;
req.push("get", "mykey");
std::cerr << "Running test case: " << tc.name << std::endl;
// Issue a request without calling async_run(), so the request stays waiting forever
net::cancellation_signal sig;
bool called = false;
conn.async_exec(
req,
ignore,
net::bind_cancellation_slot(sig.slot(), [&](error_code ec, std::size_t sz) {
BOOST_TEST(ec == net::error::operation_aborted);
BOOST_TEST(sz == 0u);
called = true;
}));
// Setup
net::io_context ctx;
connection conn(ctx);
request req;
req.push("get", "mykey");
// Issue a cancellation
sig.emit(tc.cancel_type);
// Issue a request without calling async_run(), so the request stays waiting forever
net::cancellation_signal sig;
bool called = false;
conn.async_exec(
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(sz, 0u);
called = true;
}));
// Prevent the test for deadlocking in case of failure
ctx.run_for(3s);
BOOST_TEST(called);
}
// Issue a cancellation
sig.emit(tc.cancel_type);
// Prevent the test for deadlocking in case of failure
ctx.run_for(test_timeout);
BOOST_TEST(called);
}
}
// We can cancel requests that have been written but which
// responses haven't been received yet.
// Terminal and partial cancellation types are supported here.
void test_cancel_written()
{
// Setup
net::io_context ctx;
connection conn{ctx};
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds::zero();
bool run_finished = false, exec1_finished = false, exec2_finished = false,
exec3_finished = false;
// Will be cancelled after it has been written but before the
// response arrives. Create everything in dynamic memory to verify
// we don't try to access things after completion.
auto req1 = std::make_unique<request>();
req1->push("BLPOP", "any", 1);
auto r1 = std::make_unique<response<std::string>>();
// Will be cancelled too because it's sent after BLPOP.
// Tests that partial cancellation is supported, too.
request req2;
req2.push("PING", "partial_cancellation");
// Will finish successfully once the response to the BLPOP arrives
request req3;
req3.push("PING", "after_blpop");
response<std::string> r3;
// Run the connection
conn.async_run(cfg, [&](error_code ec) {
BOOST_TEST_EQ(ec, net::error::operation_aborted);
run_finished = true;
});
// The request will be cancelled before it receives a response.
// Our BLPOP will wait for longer than the timeout we're using.
// Clear allocated memory to check we don't access the request or
// response when the server response arrives.
auto blpop_cb = [&](error_code ec, std::size_t) {
req1.reset();
r1.reset();
BOOST_TEST_EQ(ec, net::error::operation_aborted);
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);
exec2_finished = true;
};
conn.async_exec(
req2,
ignore,
net::cancel_after(500ms, net::cancellation_type_t::partial, req2_cb));
// The second PING's response will be received after the BLPOP's response,
// but it will be processed successfully.
conn.async_exec(req3, r3, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST_EQ(std::get<0>(r3).value(), "after_blpop");
conn.cancel();
exec3_finished = true;
});
ctx.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec1_finished);
BOOST_TEST(exec2_finished);
BOOST_TEST(exec3_finished);
}
// Requests configured to do so are cancelled if the connection
// hasn't been established when they are executed
void test_cancel_if_not_connected()
{
net::io_context ioc;
connection conn{ioc};
request req;
req.get_config().cancel_if_not_connected = true;
req.push("PING");
bool exec_finished = false;
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error::not_connected);
exec_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
}
// Requests configured to do so are cancelled when the connection is lost.
// Tests with a written request that hasn't been responded yet
void test_cancel_on_connection_lost_written()
{
// Setup
net::io_context ioc;
connection conn{ioc};
// req0 and req1 will be coalesced together. When req0
// completes, we know that req1 will be waiting for a response.
// req1 will block forever.
request req0;
req0.push("PING");
request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.get_config().cancel_if_unresponded = true;
req1.push("BLPOP", "any", 0);
bool run_finished = false, exec0_finished = false, exec1_finished = false;
// Run the connection
auto cfg = make_test_config();
conn.async_run(cfg, [&](error_code ec) {
BOOST_TEST_EQ(ec, net::error::operation_aborted);
run_finished = true;
});
// Execute both requests
conn.async_exec(req0, ignore, [&](error_code ec, std::size_t) {
// The request finished successfully
BOOST_TEST_EQ(ec, error_code());
exec0_finished = true;
// We know that req1 has been written to the server, too. Trigger a cancellation
conn.cancel(operation::run);
conn.cancel(operation::reconnection);
});
conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, net::error::operation_aborted);
exec1_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec0_finished);
BOOST_TEST(exec1_finished);
}
// connection::cancel(operation::exec) works. Pending requests are cancelled,
// but written requests are not
void test_cancel_operation_exec()
{
// Setup
net::io_context ctx;
connection conn{ctx};
bool run_finished = false, exec0_finished = false, exec1_finished = false,
exec2_finished = false;
request req0;
req0.push("PING", "before_blpop");
request req1;
req1.push("BLPOP", "any", 1);
generic_response r1;
request req2;
req2.push("PING", "after_blpop");
// Run the connection
conn.async_run(make_test_config(), [&](error_code ec) {
BOOST_TEST_EQ(ec, net::error::operation_aborted);
run_finished = true;
});
// Execute req0 and req1. They will be coalesced together.
// When req0 completes, we know that req1 will be waiting its response
conn.async_exec(req0, ignore, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
exec0_finished = true;
conn.cancel(operation::exec);
});
// By default, ignore will issue an error when a NULL is received.
// ATM, this causes the connection to be torn down. Using a generic_response avoids this.
// See https://github.com/boostorg/redis/issues/314
conn.async_exec(req1, r1, [&](error_code ec, std::size_t) {
// No error should occur since the cancellation should be ignored
std::cout << "async_exec (1): " << ec.message() << std::endl;
BOOST_TEST_EQ(ec, error_code());
exec1_finished = true;
// The connection remains usable
conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) {
BOOST_TEST_EQ(ec2, error_code());
exec2_finished = true;
conn.cancel();
});
});
ctx.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec0_finished);
BOOST_TEST(exec1_finished);
BOOST_TEST(exec2_finished);
}
} // namespace
#else
BOOST_AUTO_TEST_CASE(dummy) { }
#endif
int main()
{
test_cancel_pending();
test_cancel_written();
test_cancel_if_not_connected();
test_cancel_on_connection_lost_written();
test_cancel_operation_exec();
return boost::report_errors();
}

View File

@@ -1,95 +0,0 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <cstddef>
#define BOOST_TEST_MODULE conn_exec_cancel
#include <boost/test/included/unit_test.hpp>
#include "common.hpp"
#include <iostream>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
// NOTE1: Sends hello separately. I have observed that if hello and
// blpop are sent toguether, Redis will send the response of hello
// right away, not waiting for blpop. That is why we have to send it
// separately.
namespace net = boost::asio;
using error_code = boost::system::error_code;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::logger;
using boost::redis::connection;
using namespace std::chrono_literals;
namespace {
auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
generic_response gresp;
auto conn = std::make_shared<connection>(ex);
run(conn);
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
// See NOTE1.
request req0;
req0.push("PING", "async_ignore_explicit_cancel_of_req_written");
co_await conn->async_exec(req0, gresp);
request req1;
req1.push("BLPOP", "any", 3);
bool seen = false;
conn->async_exec(req1, gresp, [&](error_code ec, std::size_t) {
// No error should occur since the cancellation should be ignored
std::cout << "async_exec (1): " << ec.message() << std::endl;
BOOST_TEST(ec == error_code());
seen = true;
});
// Will complete while BLPOP is pending.
error_code ec;
co_await st.async_wait(net::redirect_error(ec));
conn->cancel(operation::exec);
BOOST_TEST(ec == error_code());
request req2;
req2.push("PING");
// Test whether the connection remains usable after a call to
// cancel(exec).
co_await conn->async_exec(req2, gresp, net::redirect_error(ec));
conn->cancel();
BOOST_TEST(ec == error_code());
BOOST_TEST(seen);
}
BOOST_AUTO_TEST_CASE(test_ignore_explicit_cancel_of_req_written)
{
run_coroutine_test(async_ignore_explicit_cancel_of_req_written());
}
} // namespace
#else
BOOST_AUTO_TEST_CASE(dummy) { }
#endif

View File

@@ -292,4 +292,38 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
BOOST_TEST(c3_called);
}
BOOST_AUTO_TEST_CASE(issue_287_generic_response_error_then_success)
{
// Setup
auto cfg = make_test_config();
request req;
req.push("PING", "hello");
req.push("set", "mykey"); // This command has a missing argument and will cause an error
req.push("get", "mykey"); // This one is okay
generic_response resp;
// I/O objects
net::io_context ioc;
connection conn{ioc};
bool run_finished = false, exec_finished = false;
conn.async_run(cfg, [&](error_code ec) {
BOOST_TEST(ec == net::error::operation_aborted);
run_finished = true;
});
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
exec_finished = true;
conn.cancel();
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
BOOST_TEST(resp.has_error());
BOOST_TEST(resp.error().diagnostic == "ERR wrong number of arguments for 'set' command");
}
} // namespace

View File

@@ -30,7 +30,7 @@ using namespace std::chrono_literals;
namespace {
BOOST_AUTO_TEST_CASE(request_retry_false)
BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_true)
{
request req0;
req0.get_config().cancel_on_connection_lost = true;
@@ -105,8 +105,12 @@ BOOST_AUTO_TEST_CASE(request_retry_false)
BOOST_TEST(run_finished);
}
BOOST_AUTO_TEST_CASE(request_retry_true)
BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_false)
{
// The BLPOP request will block forever, causing the health checker
// to trigger a reconnection. Although req2 has been written,
// it has cancel_if_unresponded=false, so it will be retried
// after reconnection
request req0;
req0.get_config().cancel_on_connection_lost = true;
req0.push("HELLO", 3);
@@ -126,23 +130,10 @@ BOOST_AUTO_TEST_CASE(request_retry_true)
req3.push("QUIT");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
auto conn = std::make_shared<connection>(ioc, logger::level::debug);
net::steady_timer st{ioc};
bool timer_finished = false, c0_called = false, c1_called = false, c2_called = false,
c3_called = false, run_finished = false;
st.expires_after(std::chrono::seconds{1});
st.async_wait([&](error_code ec) {
// Cancels the request before receiving the response. This
// should cause the third request to not complete with error
// since it has cancel_if_unresponded = true and cancellation
// comes after it was written.
timer_finished = true;
BOOST_TEST(ec == error_code());
conn->cancel(operation::run);
});
bool c0_called = false, c1_called = false, c2_called = false, c3_called = false,
run_finished = false;
auto c3 = [&](error_code ec, std::size_t) {
c3_called = true;
@@ -172,8 +163,8 @@ BOOST_AUTO_TEST_CASE(request_retry_true)
conn->async_exec(req0, ignore, c0);
auto cfg = make_test_config();
cfg.health_check_interval = 5s;
conn->async_run(cfg, {}, [&](error_code ec) {
cfg.health_check_interval = 200ms;
conn->async_run(cfg, [&](error_code ec) {
run_finished = true;
std::cout << ec.message() << std::endl;
BOOST_TEST(ec != error_code());
@@ -181,7 +172,6 @@ BOOST_AUTO_TEST_CASE(request_retry_true)
ioc.run_for(test_timeout);
BOOST_TEST(timer_finished);
BOOST_TEST(c0_called);
BOOST_TEST(c1_called);
BOOST_TEST(c2_called);

View File

@@ -28,10 +28,6 @@ using namespace boost::redis;
namespace {
// user tests
// logging can be disabled
// logging can be changed verbosity
template <class Conn>
void run_with_invalid_config(net::io_context& ioc, Conn& conn)
{

121
test/test_conn_monitor.cpp Normal file
View File

@@ -0,0 +1,121 @@
/* 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 <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
#include <cstddef>
namespace net = boost::asio;
using boost::system::error_code;
using boost::redis::connection;
using boost::redis::request;
using boost::redis::ignore;
using boost::redis::operation;
using boost::redis::generic_response;
using boost::redis::consume_one;
using namespace std::chrono_literals;
namespace {
// Verifies that using the MONITOR command works properly.
// Opens a connection, issues a MONITOR, issues some commands to
// generate some traffic, and waits for several MONITOR messages to arrive.
class test_monitor {
net::io_context ioc;
connection conn{ioc};
generic_response monitor_resp;
request ping_req;
bool run_finished = false, exec_finished = false, receive_finished = false;
int num_pushes_received = 0;
void start_receive()
{
conn.async_receive([this](error_code ec, std::size_t) {
// We should expect one push entry, at least
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST(monitor_resp.has_value());
BOOST_TEST_NOT(monitor_resp.value().empty());
// Log the value and consume it
std::clog << "Event> " << monitor_resp.value().front().value << std::endl;
consume_one(monitor_resp);
if (++num_pushes_received >= 5) {
receive_finished = true;
} else {
start_receive();
}
});
}
// Starts generating traffic so our receiver task can progress
void start_generating_traffic()
{
conn.async_exec(ping_req, ignore, [this](error_code ec, std::size_t) {
// PINGs should complete successfully
BOOST_TEST_EQ(ec, error_code());
// Once the receiver exits, stop sending requests and tear down the connection
if (receive_finished) {
conn.cancel();
exec_finished = true;
} else {
start_generating_traffic();
}
});
}
public:
test_monitor() = default;
void run()
{
// Setup
ping_req.push("PING", "test_monitor");
conn.set_receive_response(monitor_resp);
request monitor_req;
monitor_req.push("MONITOR");
// Run the connection
conn.async_run(make_test_config(), [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
// Issue the monitor, then start generating traffic
conn.async_exec(monitor_req, ignore, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
start_generating_traffic();
});
// In parallel, start a subscriber
start_receive();
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(receive_finished);
BOOST_TEST(exec_finished);
}
};
} // namespace
int main()
{
test_monitor{}.run();
return boost::report_errors();
}

112
test/test_conn_move.cpp Normal file
View File

@@ -0,0 +1,112 @@
//
// 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 <boost/redis/connection.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
#include <cstddef>
#include <string>
using boost::system::error_code;
namespace net = boost::asio;
using namespace boost::redis;
namespace {
// Move constructing a connection doesn't leave dangling pointers
void test_conn_move_construct()
{
// Setup
net::io_context ioc;
connection conn_prev(ioc);
connection conn(std::move(conn_prev));
request req;
req.push("PING", "something");
response<std::string> res;
bool run_finished = false, exec_finished = false;
// Run the connection
conn.async_run(make_test_config(), [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
// Launch a PING
conn.async_exec(req, res, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
});
ioc.run_for(test_timeout);
// Check
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
BOOST_TEST_EQ(std::get<0>(res).value(), "something");
}
// Moving a connection is safe even when it's running,
// and it doesn't leave dangling pointers
void test_conn_move_assign_while_running()
{
// Setup
net::io_context ioc;
connection conn(ioc);
connection conn2(ioc); // will be assigned to
request req;
req.push("PING", "something");
response<std::string> res;
bool run_finished = false, exec_finished = false;
// Run the connection
conn.async_run(make_test_config(), [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
});
// Launch a PING. When it finishes, conn will be moved-from, and conn2 will be valid
conn.async_exec(req, res, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn2.cancel();
});
// While the operations are running, perform a move
net::post(net::bind_executor(ioc.get_executor(), [&] {
conn2 = std::move(conn);
}));
ioc.run_for(test_timeout);
// Check
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
BOOST_TEST_EQ(std::get<0>(res).value(), "something");
}
} // namespace
int main()
{
test_conn_move_construct();
test_conn_move_assign_while_running();
return boost::report_errors();
}

View File

@@ -6,10 +6,14 @@
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/experimental/channel_error.hpp>
#include <boost/system/errc.hpp>
#include <string>
#define BOOST_TEST_MODULE conn_push
#include <boost/test/included/unit_test.hpp>
@@ -180,19 +184,15 @@ struct response_error_tag { };
response_error_tag error_tag_obj;
struct response_error_adapter {
void operator()(
std::size_t,
void on_init() { }
void on_done() { }
void on_node(
boost::redis::resp3::basic_node<std::string_view> const&,
boost::system::error_code& ec)
{
ec = boost::redis::error::incompatible_size;
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{
return static_cast<std::size_t>(-1);
}
};
auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; }
@@ -214,7 +214,6 @@ BOOST_AUTO_TEST_CASE(test_push_adapter)
conn->async_receive([&, conn](error_code ec, std::size_t) {
BOOST_CHECK_EQUAL(ec, boost::asio::experimental::error::channel_cancelled);
conn->cancel(operation::reconnection);
push_received = true;
});
@@ -224,7 +223,8 @@ BOOST_AUTO_TEST_CASE(test_push_adapter)
});
auto cfg = make_test_config();
conn->async_run(cfg, {}, [&run_finished](error_code ec) {
cfg.reconnect_wait_interval = 0s;
conn->async_run(cfg, [&run_finished](error_code ec) {
BOOST_CHECK_EQUAL(ec, redis::error::incompatible_size);
run_finished = true;
});
@@ -331,4 +331,71 @@ BOOST_AUTO_TEST_CASE(many_subscribers)
BOOST_TEST(finished);
}
BOOST_AUTO_TEST_CASE(test_unsubscribe)
{
net::io_context ioc;
connection conn{ioc};
// 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");
// 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");
// Finally, ping to verify that the connection is still usable
request req_ping;
req_ping.push("PING", "test_unsubscribe");
response<std::string> resp_subscribe, resp_unsubscribe, resp_ping;
bool subscribe_finished = false, unsubscribe_finished = false, ping_finished = false,
run_finished = false;
auto on_ping = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
ping_finished = true;
BOOST_TEST(std::get<0>(resp_ping).has_value());
BOOST_TEST(std::get<0>(resp_ping).value() == "test_unsubscribe");
conn.cancel();
};
auto on_unsubscribe = [&](error_code ec, std::size_t) {
unsubscribe_finished = true;
BOOST_TEST(ec == error_code());
BOOST_TEST(std::get<0>(resp_unsubscribe).has_value());
BOOST_TEST(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub") == "2");
BOOST_TEST(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub") == "1");
conn.async_exec(req_ping, resp_ping, on_ping);
};
auto on_subscribe = [&](error_code ec, std::size_t) {
subscribe_finished = true;
BOOST_TEST(ec == error_code());
BOOST_TEST(std::get<0>(resp_subscribe).has_value());
BOOST_TEST(find_client_info(std::get<0>(resp_subscribe).value(), "sub") == "3");
BOOST_TEST(find_client_info(std::get<0>(resp_subscribe).value(), "psub") == "2");
conn.async_exec(req_unsubscribe, resp_unsubscribe, on_unsubscribe);
};
conn.async_exec(req_subscribe, resp_subscribe, on_subscribe);
conn.async_run(make_test_config(), [&run_finished](error_code ec) {
BOOST_TEST(ec == net::error::operation_aborted);
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(subscribe_finished);
BOOST_TEST(unsubscribe_finished);
BOOST_TEST(ping_finished);
BOOST_TEST(run_finished);
}
} // namespace

View File

@@ -43,6 +43,7 @@ BOOST_AUTO_TEST_CASE(test_async_run_exits)
// Should fail since this request will be sent after quit.
request req3;
req3.get_config().cancel_if_not_connected = true;
req3.get_config().cancel_on_connection_lost = true;
req3.push("PING");
bool c1_called = false, c2_called = false, c3_called = false;

View File

@@ -42,8 +42,9 @@ net::awaitable<void> test_reconnect_impl()
// cancel_on_connection_lost is required because async_run might detect the failure
// after the 2nd async_exec is issued
request regular_req;
regular_req.push("GET", "mykey");
regular_req.push("PING", "SomeValue");
regular_req.get_config().cancel_on_connection_lost = false;
regular_req.get_config().cancel_if_unresponded = false;
auto conn = std::make_shared<connection>(ex);
auto cfg = make_test_config();
@@ -54,16 +55,14 @@ net::awaitable<void> test_reconnect_impl()
BOOST_TEST_CONTEXT("i=" << i)
{
// Issue a quit request, which will cause the server to close the connection.
// This request will fail
// This request will succeed, since this happens before the connection is lost.
error_code ec;
co_await conn->async_exec(quit_req, ignore, net::redirect_error(ec));
BOOST_TEST(ec == error_code());
// This should trigger reconnection, which will now succeed.
// We should be able to execute requests successfully now.
// TODO: this is currently unreliable - find our why and fix
// Reconnection will happen, and this request will succeed, too.
co_await conn->async_exec(regular_req, ignore, net::redirect_error(ec));
// BOOST_TEST(ec == error_code());
BOOST_TEST(ec == error_code());
}
}

View File

@@ -0,0 +1,88 @@
//
// 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 <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
#include <cstddef>
#include <iostream>
#include <string_view>
using boost::system::error_code;
namespace net = boost::asio;
using namespace boost::redis;
namespace {
// Terminal and partial cancellation work for async_run
template <class Connection>
void test_per_operation_cancellation(std::string_view name, net::cancellation_type_t cancel_type)
{
std::cerr << "Running test case: " << name << std::endl;
// Setup
net::io_context ioc;
Connection conn{ioc};
net::cancellation_signal sig;
request req;
req.push("PING", "something");
bool run_finished = false, exec_finished = false;
// Run the connection
auto run_cb = [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, net::error::operation_aborted);
};
conn.async_run(make_test_config(), net::bind_cancellation_slot(sig.slot(), run_cb));
// Launch a PING
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
sig.emit(cancel_type);
});
ioc.run_for(test_timeout);
// Check
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
}
} // namespace
int main()
{
using basic_connection_t = basic_connection<net::io_context::executor_type>;
test_per_operation_cancellation<basic_connection_t>(
"basic_connection, terminal",
net::cancellation_type_t::terminal);
test_per_operation_cancellation<basic_connection_t>(
"basic_connection, partial",
net::cancellation_type_t::partial);
test_per_operation_cancellation<connection>(
"connection, terminal",
net::cancellation_type_t::terminal);
test_per_operation_cancellation<connection>(
"connection, partial",
net::cancellation_type_t::partial);
return boost::report_errors();
}

329
test/test_conn_setup.cpp Normal file
View File

@@ -0,0 +1,329 @@
//
// 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 <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "common.hpp"
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
namespace asio = boost::asio;
namespace redis = boost::redis;
using namespace std::chrono_literals;
using boost::system::error_code;
namespace {
// Creates a user with a known password. Harmless if the user already exists
void setup_password()
{
// Setup
asio::io_context ioc;
redis::connection conn{ioc};
// Enable the user and grant them permissions on everything
redis::request req;
req.push("ACL", "SETUSER", "myuser", "on", ">mypass", "~*", "&*", "+@all");
redis::generic_response resp;
bool run_finished = false, exec_finished = false;
conn.async_run(make_test_config(), [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
});
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
BOOST_TEST(resp.has_value());
}
void test_auth_success()
{
// Setup
asio::io_context ioc;
redis::connection conn{ioc};
// This request should return the username we're logged in as
redis::request req;
req.push("ACL", "WHOAMI");
redis::response<std::string> resp;
// These credentials are set up in main, before tests are run
auto cfg = make_test_config();
cfg.username = "myuser";
cfg.password = "mypass";
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
});
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser");
}
void test_auth_failure()
{
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
std::ostringstream oss;
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
oss << msg << '\n';
});
// Setup
asio::io_context ioc;
redis::connection conn{ioc, std::move(lgr)};
// 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;
bool run_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, redis::error::resp3_hello);
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
// Check the log
auto log = oss.str();
if (!BOOST_TEST_NE(log.find("WRONGPASS"), std::string::npos)) {
std::cerr << "Log was: " << log << std::endl;
}
}
void test_database_index()
{
// Setup
asio::io_context ioc;
redis::connection conn(ioc);
// Use a non-default database index
auto cfg = make_test_config();
cfg.database_index = 2;
redis::request req;
req.push("CLIENT", "INFO");
redis::response<std::string> resp;
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t n) {
BOOST_TEST_EQ(ec, error_code());
std::clog << "async_exec has completed: " << n << std::endl;
conn.cancel();
exec_finished = true;
});
conn.async_run(cfg, {}, [&run_finished](error_code) {
std::clog << "async_run has exited." << std::endl;
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2");
}
// The user configured an empty setup request. No request should be sent
void test_setup_empty()
{
// Setup
asio::io_context ioc;
redis::connection conn(ioc);
auto cfg = make_test_config();
cfg.use_setup = true;
cfg.setup.clear();
redis::request req;
req.push("CLIENT", "INFO");
redis::response<std::string> resp;
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
exec_finished = true;
});
conn.async_run(cfg, {}, [&run_finished](error_code) {
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2
}
// We can use the setup member to run commands at startup
void test_setup_hello()
{
// Setup
asio::io_context ioc;
redis::connection conn(ioc);
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);
redis::request req;
req.push("CLIENT", "INFO");
redis::response<std::string> resp;
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
exec_finished = true;
});
conn.async_run(cfg, {}, [&run_finished](error_code) {
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
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");
}
// Running a pipeline without a HELLO is okay (regression check: we set the priority flag)
void test_setup_no_hello()
{
// Setup
asio::io_context ioc;
redis::connection conn(ioc);
auto cfg = make_test_config();
cfg.use_setup = true;
cfg.setup.clear();
cfg.setup.push("SELECT", 8);
redis::request req;
req.push("CLIENT", "INFO");
redis::response<std::string> resp;
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
exec_finished = true;
});
conn.async_run(cfg, {}, [&run_finished](error_code) {
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP3
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8");
}
void test_setup_failure()
{
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
std::ostringstream oss;
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
oss << msg << '\n';
});
// Setup
asio::io_context ioc;
redis::connection conn{ioc, std::move(lgr)};
// 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;
bool run_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, redis::error::resp3_hello);
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
// Check the log
auto log = oss.str();
if (!BOOST_TEST_NE(log.find("wrong number of arguments"), std::string::npos)) {
std::cerr << "Log was: " << log << std::endl;
}
}
} // namespace
int main()
{
setup_password();
test_auth_success();
test_auth_failure();
test_database_index();
test_setup_empty();
test_setup_hello();
test_setup_no_hello();
test_setup_failure();
return boost::report_errors();
}

View File

@@ -152,6 +152,8 @@ BOOST_AUTO_TEST_CASE(reconnection)
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;
request quit_request;
quit_request.push("QUIT");
@@ -173,12 +175,6 @@ BOOST_AUTO_TEST_CASE(reconnection)
auto quit_callback = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
// If a request is issued immediately after QUIT, the request sometimes
// fails, probably due to a race condition. This dispatches any pending
// handlers, triggering the reconnection process.
// TODO: this should not be required.
ioc.poll();
conn.async_exec(ping_request, ignore, ping_callback);
};

631
test/test_connect_fsm.cpp Normal file
View File

@@ -0,0 +1,631 @@
//
// 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 <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/core/lightweight_test.hpp>
#include "sansio_utils.hpp"
#include <iterator>
#include <ostream>
#include <string>
#include <string_view>
#include <vector>
using namespace boost::redis;
namespace asio = boost::asio;
using detail::connect_fsm;
using detail::connect_action_type;
using detail::connect_action;
using detail::buffered_logger;
using detail::redis_stream_state;
using detail::transport_type;
using asio::ip::tcp;
using boost::system::error_code;
using boost::asio::cancellation_type_t;
using resolver_results = tcp::resolver::results_type;
// Operators
static const char* to_string(connect_action_type type)
{
switch (type) {
case connect_action_type::unix_socket_close: return "connect_action_type::unix_socket_close";
case connect_action_type::unix_socket_connect:
return "connect_action_type::unix_socket_connect";
case connect_action_type::tcp_resolve: return "connect_action_type::tcp_resolve";
case connect_action_type::tcp_connect: return "connect_action_type::tcp_connect";
case connect_action_type::ssl_stream_reset: return "connect_action_type::ssl_stream_reset";
case connect_action_type::ssl_handshake: return "connect_action_type::ssl_handshake";
case connect_action_type::done: return "connect_action_type::done";
default: return "<unknown connect_action_type>";
}
}
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 "<unknown transport_type>";
}
}
namespace boost::redis::detail {
std::ostream& operator<<(std::ostream& os, connect_action_type type)
{
return os << to_string(type);
}
std::ostream& operator<<(std::ostream& os, transport_type type) { return os << to_string(type); }
bool operator==(const connect_action& lhs, const connect_action& rhs) noexcept
{
return lhs.type == rhs.type && lhs.ec == rhs.ec;
}
std::ostream& operator<<(std::ostream& os, const connect_action& act)
{
os << "connect_action{ .type=" << act.type;
if (act.type == connect_action_type::done)
os << ", .error=" << act.ec;
return os << " }";
}
} // namespace boost::redis::detail
namespace {
// TCP endpoints
const tcp::endpoint endpoint(asio::ip::make_address("192.168.10.1"), 1234);
const tcp::endpoint endpoint2(asio::ip::make_address("192.168.10.2"), 1235);
auto resolver_data = [] {
const tcp::endpoint data[] = {endpoint, endpoint2};
return asio::ip::tcp::resolver::results_type::create(
std::begin(data),
std::end(data),
"my_host",
"1234");
}();
// Reduce duplication
struct fixture : detail::log_fixture {
config cfg;
buffered_logger lgr{make_logger()};
connect_fsm fsm{cfg, lgr};
redis_stream_state st{};
fixture(config&& cfg = {})
: cfg{std::move(cfg)}
{ }
};
config make_ssl_config()
{
config cfg;
cfg.use_ssl = true;
return cfg;
}
config make_unix_config()
{
config cfg;
cfg.unix_socket = "/run/redis.sock";
return cfg;
}
void test_tcp_success()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::done);
// The transport type was appropriately set
BOOST_TEST_EQ(fix.st.type, transport_type::tcp);
BOOST_TEST_NOT(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Connected to 192.168.10.1:1234" },
});
}
void test_tcp_tls_success()
{
// Setup
fixture fix{make_ssl_config()};
// Run the algorithm. No SSL stream reset is performed here
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::done);
// The transport type was appropriately set
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
BOOST_TEST(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Connected to 192.168.10.1:1234" },
{logger::level::info, "Successfully performed SSL handshake" },
});
}
void test_tcp_tls_success_reconnect()
{
// Setup
fixture fix{make_ssl_config()};
fix.st.ssl_stream_used = true;
// Run the algorithm. The stream is used, so it needs to be reset
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_stream_reset);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::done);
// The transport type was appropriately set
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
BOOST_TEST(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Connected to 192.168.10.1:1234" },
{logger::level::info, "Successfully performed SSL handshake" },
});
}
void test_unix_success()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::done);
// The transport type was appropriately set
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
BOOST_TEST_NOT(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
{logger::level::info, "Connected to /run/redis.sock"},
});
}
// Close errors are ignored
void test_unix_success_close_error()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(asio::error::bad_descriptor, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::done);
// The transport type was appropriately set
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
BOOST_TEST_NOT(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
{logger::level::info, "Connected to /run/redis.sock"},
});
}
// Resolve errors
void test_tcp_resolve_error()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error::empty_field, resolver_results{}, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Error resolving the server hostname: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
void test_tcp_resolve_timeout()
{
// Setup
fixture fix;
// Since we use cancel_after, a timeout is an operation_aborted without a cancellation state set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(
asio::error::operation_aborted,
resolver_results{},
fix.st,
cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::resolve_timeout));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Error resolving the server hostname: Resolve timeout. [boost.redis:17]"},
// clang-format on
});
}
void test_tcp_resolve_cancel()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(
asio::error::operation_aborted,
resolver_results{},
fix.st,
cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging here is system-dependent, so we don't check the message
BOOST_TEST_EQ(fix.msgs.size(), 1u);
}
void test_tcp_resolve_cancel_edge()
{
// Setup
fixture fix;
// Cancel state set but no error
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_results{}, fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging here is system-dependent, so we don't check the message
BOOST_TEST_EQ(fix.msgs.size(), 1u);
}
// Connect errors
void test_tcp_connect_error()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error::empty_field, tcp::endpoint{}, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
void test_tcp_connect_timeout()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(
asio::error::operation_aborted,
tcp::endpoint{},
fix.st,
cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
// clang-format on
});
}
void test_tcp_connect_cancel()
{
// Setup
fixture fix;
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(
asio::error::operation_aborted,
tcp::endpoint{},
fix.st,
cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging here is system-dependent, so we don't check the message
BOOST_TEST_EQ(fix.msgs.size(), 2u);
}
void test_tcp_connect_cancel_edge()
{
// Setup
fixture fix;
// Run the algorithm. Cancellation state set but no error
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), tcp::endpoint{}, fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging here is system-dependent, so we don't check the message
BOOST_TEST_EQ(fix.msgs.size(), 2u);
}
// SSL handshake error
void test_ssl_handshake_error()
{
// Setup
fixture fix{make_ssl_config()};
// Run the algorithm. No SSL stream reset is performed here
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(error::empty_field, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// The stream is marked as used
BOOST_TEST(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Connected to 192.168.10.1:1234" },
{logger::level::info, "Failed to perform SSL handshake: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
void test_ssl_handshake_timeout()
{
// Setup
fixture fix{make_ssl_config()};
// Run the algorithm. Timeout = operation_aborted without the cancel type set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(asio::error::operation_aborted, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::ssl_handshake_timeout));
// The stream is marked as used
BOOST_TEST(fix.st.ssl_stream_used);
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
{logger::level::info, "Connected to 192.168.10.1:1234" },
{logger::level::info, "Failed to perform SSL handshake: SSL handshake timeout. [boost.redis:20]"},
// clang-format on
});
}
void test_ssl_handshake_cancel()
{
// Setup
fixture fix{make_ssl_config()};
// Run the algorithm. Cancel = operation_aborted with the cancel type set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(asio::error::operation_aborted, fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// The stream is marked as used
BOOST_TEST(fix.st.ssl_stream_used);
// Logging is system-dependent, so we don't check messages
BOOST_TEST_EQ(fix.msgs.size(), 3u);
}
void test_ssl_handshake_cancel_edge()
{
// Setup
fixture fix{make_ssl_config()};
// Run the algorithm. No error, but the cancel state is set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_resolve);
act = fix.fsm.resume(error_code(), resolver_data, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::tcp_connect);
act = fix.fsm.resume(error_code(), endpoint, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::ssl_handshake);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// The stream is marked as used
BOOST_TEST(fix.st.ssl_stream_used);
// Logging is system-dependent, so we don't check messages
BOOST_TEST_EQ(fix.msgs.size(), 3u);
}
// UNIX connect errors
void test_unix_connect_error()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(error::empty_field, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
void test_unix_connect_timeout()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm. Timeout = operation_aborted without a cancel state
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(asio::error::operation_aborted, fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
// clang-format on
});
}
void test_unix_connect_cancel()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm. Cancel = operation_aborted with a cancel state
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(asio::error::operation_aborted, fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging is system-dependent
BOOST_TEST_EQ(fix.msgs.size(), 1u);
}
void test_unix_connect_cancel_edge()
{
// Setup
fixture fix{make_unix_config()};
// Run the algorithm. No error, but cancel state is set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_close);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
BOOST_TEST_EQ(act, connect_action_type::unix_socket_connect);
act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// Logging is system-dependent
BOOST_TEST_EQ(fix.msgs.size(), 1u);
}
} // namespace
int main()
{
test_tcp_success();
test_tcp_tls_success();
test_tcp_tls_success_reconnect();
test_unix_success();
test_unix_success_close_error();
test_tcp_resolve_error();
test_tcp_resolve_timeout();
test_tcp_resolve_cancel();
test_tcp_resolve_cancel_edge();
test_tcp_connect_error();
test_tcp_connect_timeout();
test_tcp_connect_cancel();
test_tcp_connect_cancel_edge();
test_ssl_handshake_error();
test_ssl_handshake_timeout();
test_ssl_handshake_cancel();
test_ssl_handshake_cancel_edge();
test_unix_connect_error();
test_unix_connect_timeout();
test_unix_connect_cancel();
test_unix_connect_cancel_edge();
return boost::report_errors();
}

View File

@@ -12,12 +12,14 @@
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "sansio_utils.hpp"
#include <cstddef>
#include <memory>
#include <optional>
#include <ostream>
#include <utility>
@@ -26,15 +28,29 @@ namespace asio = boost::asio;
using detail::exec_fsm;
using detail::multiplexer;
using detail::exec_action_type;
using detail::consume_result;
using detail::exec_action;
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
static auto to_string(exec_action_type t) noexcept -> char const*
{
switch (t) {
BOOST_REDIS_EXEC_SWITCH_CASE(setup_cancellation);
BOOST_REDIS_EXEC_SWITCH_CASE(immediate);
BOOST_REDIS_EXEC_SWITCH_CASE(done);
BOOST_REDIS_EXEC_SWITCH_CASE(notify_writer);
BOOST_REDIS_EXEC_SWITCH_CASE(wait_for_response);
default: return "exec_action_type::<invalid type>";
}
}
// Operators
namespace boost::redis::detail {
extern auto to_string(exec_action_type t) noexcept -> char const*;
std::ostream& operator<<(std::ostream& os, exec_action_type type)
{
os << to_string(type);
@@ -59,6 +75,16 @@ std::ostream& operator<<(std::ostream& os, exec_action act)
return os << " }";
}
std::ostream& operator<<(std::ostream& os, consume_result v)
{
switch (v) {
case consume_result::needs_more: return os << "consume_result::needs_more";
case consume_result::got_response: return os << "consume_result::got_response";
case consume_result::got_push: return os << "consume_result::got_push";
default: return os << "<unknown consume_result>";
}
}
} // namespace boost::redis::detail
// Prints a message on failure. Useful for parameterized tests
@@ -81,10 +107,8 @@ struct elem_and_request {
{
// Empty requests are not valid. The request needs to be populated before creating the element
req.push("get", "mykey");
elm = std::make_shared<multiplexer::elem>(req, any_adapter{});
elm = std::make_shared<multiplexer::elem>(
req,
[](std::size_t, resp3::node_view const&, error_code&) { });
elm->set_done_callback([this] {
++done_calls;
});
@@ -114,14 +138,14 @@ void test_success()
// Simulate a successful write
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
BOOST_TEST_EQ(mpx.commit_write(), 0u); // all requests expect a response
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
// Simulate a successful read
mpx.get_read_buffer() = "$5\r\nhello\r\n";
auto req_status = mpx.consume_next(ec);
read(mpx, "$5\r\nhello\r\n");
auto req_status = mpx.consume(ec);
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST_EQ(req_status.first.value(), false); // it wasn't a push
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
BOOST_TEST_EQ(input.done_calls, 1u);
// This will awaken the exec operation, and should complete the operation
@@ -153,16 +177,16 @@ void test_parse_error()
// Simulate a successful write
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
BOOST_TEST_EQ(mpx.commit_write(), 0u); // all requests expect a response
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
// Simulate a read that will trigger an error.
// The second field should be a number (rather than the empty string).
// Note that although part of the buffer was consumed, the multiplexer
// currently throws this information away.
mpx.get_read_buffer() = "*2\r\n$5\r\nhello\r\n:\r\n";
auto req_status = mpx.consume_next(ec);
read(mpx, "*2\r\n$5\r\nhello\r\n:\r\n");
auto req_status = mpx.consume(ec);
BOOST_TEST_EQ(ec, error::empty_field);
BOOST_TEST_EQ(req_status.second, 0u);
BOOST_TEST_EQ(req_status.second, 15u);
BOOST_TEST_EQ(input.done_calls, 1u);
// This will awaken the exec operation, and should complete the operation
@@ -215,14 +239,14 @@ void test_not_connected()
// Simulate a successful write
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
BOOST_TEST_EQ(mpx.commit_write(), 0u); // all requests expect a response
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
// Simulate a successful read
mpx.get_read_buffer() = "$5\r\nhello\r\n";
auto req_status = mpx.consume_next(ec);
read(mpx, "$5\r\nhello\r\n");
auto req_status = mpx.consume(ec);
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST_EQ(req_status.first.value(), false); // it wasn't a push
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
BOOST_TEST_EQ(input.done_calls, 1u);
// This will awaken the exec operation, and should complete the operation
@@ -276,56 +300,26 @@ void test_cancel_waiting()
}
}
// If the request is being processed and terminal cancellation got requested, we cancel the connection
void test_cancel_notwaiting_terminal()
{
// Setup
multiplexer mpx;
elem_and_request input;
exec_fsm fsm(mpx, std::move(input.elm));
// Initiate
auto act = fsm.resume(false, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
// The multiplexer starts writing the request
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
// A cancellation arrives
act = fsm.resume(true, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, exec_action_type::cancel_run);
act = fsm.resume(true, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted));
// The object needs to survive here, otherwise an inconsistent connection state is created
}
// If the request is being processed and other types of cancellation got requested, we ignore the cancellation
void test_cancel_notwaiting_notterminal()
// If the request is being processed and terminal or partial
// cancellation is requested, we mark the request as abandoned
void test_cancel_notwaiting_terminal_partial()
{
constexpr struct {
const char* name;
asio::cancellation_type_t type;
} test_cases[] = {
{"partial", asio::cancellation_type_t::partial },
{"total", asio::cancellation_type_t::total },
{"mixed", asio::cancellation_type_t::partial | asio::cancellation_type_t::total},
{"terminal", asio::cancellation_type_t::terminal},
{"partial", asio::cancellation_type_t::partial },
};
for (const auto& tc : test_cases) {
// Setup
multiplexer mpx;
elem_and_request input;
exec_fsm fsm(mpx, std::move(input.elm));
error_code ec;
auto input = std::make_unique<elem_and_request>();
exec_fsm fsm(mpx, std::move(input->elm));
// Initiate
auto act = fsm.resume(true, cancellation_type_t::none);
auto act = fsm.resume(false, cancellation_type_t::none);
BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name);
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name);
@@ -333,31 +327,69 @@ void test_cancel_notwaiting_notterminal()
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name);
// Simulate a successful write
// The multiplexer starts writing the request
BOOST_TEST_EQ_MSG(mpx.prepare_write(), 1u, tc.name);
BOOST_TEST_EQ_MSG(mpx.commit_write(), 0u, tc.name); // all requests expect a response
BOOST_TEST_EQ_MSG(mpx.commit_write(mpx.get_write_buffer().size()), true, tc.name);
// We got requested a cancellation here, but we can't honor it
// A cancellation arrives
act = fsm.resume(true, tc.type);
BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name);
BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted));
input.reset(); // Verify we don't access the request or response after completion
// Simulate a successful read
mpx.get_read_buffer() = "$5\r\nhello\r\n";
auto req_status = mpx.consume_next(ec);
error_code ec;
// When the response to this request arrives, it gets ignored
read(mpx, "-ERR wrong command\r\n");
auto res = mpx.consume(ec);
BOOST_TEST_EQ_MSG(ec, error_code(), tc.name);
BOOST_TEST_EQ_MSG(req_status.first.value(), false, tc.name); // it wasn't a push
BOOST_TEST_EQ_MSG(req_status.second, 11u, tc.name); // the entire buffer was consumed
BOOST_TEST_EQ_MSG(input.done_calls, 1u, tc.name);
BOOST_TEST_EQ_MSG(res.first, consume_result::got_response, tc.name);
// This will awaken the exec operation, and should complete the operation
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ_MSG(act, exec_action(error_code(), 11u), tc.name);
// All memory should have been freed by now
BOOST_TEST_EQ_MSG(input.weak_elm.expired(), true, tc.name);
// The multiplexer::elem object needs to survive here to mark the
// request as abandoned
}
}
// If the request is being processed and total cancellation is requested, we ignore the cancellation
void test_cancel_notwaiting_total()
{
// Setup
multiplexer mpx;
elem_and_request input;
exec_fsm fsm(mpx, std::move(input.elm));
error_code ec;
// Initiate
auto act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
// Simulate a successful write
BOOST_TEST_EQ(mpx.prepare_write(), 1u);
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
// We got requested a cancellation here, but we can't honor it
act = fsm.resume(true, asio::cancellation_type_t::total);
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
// Simulate a successful read
read(mpx, "$5\r\nhello\r\n");
auto req_status = mpx.consume(ec);
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
BOOST_TEST_EQ(input.done_calls, 1u);
// This will awaken the exec operation, and should complete the operation
act = fsm.resume(true, cancellation_type_t::none);
BOOST_TEST_EQ(act, exec_action(error_code(), 11u));
// All memory should have been freed by now
BOOST_TEST_EQ(input.weak_elm.expired(), true);
}
} // namespace
int main()
@@ -367,8 +399,8 @@ int main()
test_cancel_if_not_connected();
test_not_connected();
test_cancel_waiting();
test_cancel_notwaiting_terminal();
test_cancel_notwaiting_notterminal();
test_cancel_notwaiting_terminal_partial();
test_cancel_notwaiting_total();
return boost::report_errors();
}

View File

@@ -1,76 +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)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/error.hpp>
#include <boost/system/error_code.hpp>
#define BOOST_TEST_MODULE issue_181
#include <boost/test/included/unit_test.hpp>
#include "common.hpp"
#include <chrono>
#include <iostream>
namespace net = boost::asio;
using boost::redis::request;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore;
using boost::redis::logger;
using boost::redis::config;
using boost::redis::operation;
using boost::redis::connection;
using boost::system::error_code;
using namespace std::chrono_literals;
namespace {
BOOST_AUTO_TEST_CASE(issue_181)
{
using basic_connection = boost::redis::basic_connection<net::any_io_executor>;
auto const level = boost::redis::logger::level::debug;
net::io_context ioc;
auto ctx = net::ssl::context{net::ssl::context::tlsv12_client};
basic_connection conn{ioc.get_executor(), std::move(ctx)};
net::steady_timer timer{ioc};
timer.expires_after(std::chrono::seconds{1});
bool run_finished = false;
auto run_cont = [&](error_code ec) {
std::cout << "async_run1: " << ec.message() << std::endl;
BOOST_TEST(ec == net::error::operation_aborted);
run_finished = true;
};
auto cfg = make_test_config();
cfg.health_check_interval = std::chrono::seconds{0};
cfg.reconnect_wait_interval = std::chrono::seconds{0};
conn.async_run(cfg, boost::redis::logger{level}, run_cont);
BOOST_TEST(!conn.run_is_canceled());
// Uses a timer to wait some time until run has been called.
auto timer_cont = [&](error_code ec) {
std::cout << "timer_cont: " << ec.message() << std::endl;
BOOST_TEST(ec == error_code());
BOOST_TEST(!conn.run_is_canceled());
conn.cancel(operation::run);
BOOST_TEST(conn.run_is_canceled());
};
timer.async_wait(timer_cont);
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
}
} // namespace

View File

@@ -528,6 +528,8 @@ BOOST_AUTO_TEST_CASE(cover_error)
check_error("boost.redis", boost::redis::error::sync_receive_push_failed);
check_error("boost.redis", boost::redis::error::incompatible_node_depth);
check_error("boost.redis", boost::redis::error::resp3_hello);
check_error("boost.redis", boost::redis::error::exceeds_maximum_read_buffer_size);
check_error("boost.redis", boost::redis::error::write_timeout);
}
std::string get_type_as_str(boost::redis::resp3::type t)
@@ -594,8 +596,12 @@ BOOST_AUTO_TEST_CASE(adapter)
response<std::string, int, ignore_t> resp;
auto f = boost_redis_adapt(resp);
f(0, resp3::basic_node<std::string_view>{type::simple_string, 1, 0, "Hello"}, ec);
f(1, resp3::basic_node<std::string_view>{type::number, 1, 0, "42"}, ec);
f.on_init();
f.on_node(resp3::node_view{type::simple_string, 1, 0, "Hello"}, ec);
f.on_done();
f.on_init();
f.on_node(resp3::node_view{type::number, 1, 0, "42"}, ec);
f.on_done();
BOOST_CHECK_EQUAL(std::get<0>(resp).value(), "Hello");
BOOST_TEST(!ec);
@@ -613,7 +619,7 @@ BOOST_AUTO_TEST_CASE(adapter_as)
for (auto const& e : set_expected1a.value()) {
error_code ec;
adapter(e, ec);
adapter.on_node(e, ec);
}
}

View File

@@ -6,38 +6,43 @@
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/resp3_handshaker.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#define BOOST_TEST_MODULE conn_quit
#include <boost/redis/response.hpp>
#define BOOST_TEST_MODULE low_level_sync_sans_io
#include <boost/test/included/unit_test.hpp>
#include <iostream>
#include <string>
using boost::redis::request;
using boost::redis::config;
using boost::redis::detail::push_hello;
using boost::redis::response;
using boost::redis::adapter::adapt2;
using boost::redis::adapter::result;
using boost::redis::resp3::detail::deserialize;
using boost::redis::ignore_t;
using boost::redis::detail::multiplexer;
using boost::redis::generic_response;
using boost::redis::ignore_t;
using boost::redis::resp3::detail::deserialize;
using boost::redis::resp3::node;
using boost::redis::resp3::to_string;
using boost::redis::response;
using boost::redis::any_adapter;
using boost::system::error_code;
#define RESP3_SET_PART1 "~6\r\n+orange\r"
#define RESP3_SET_PART2 "\n+apple\r\n+one"
#define RESP3_SET_PART3 "\r\n+two\r"
#define RESP3_SET_PART4 "\n+three\r\n+orange\r\n"
char const* resp3_set = RESP3_SET_PART1 RESP3_SET_PART2 RESP3_SET_PART3 RESP3_SET_PART4;
BOOST_AUTO_TEST_CASE(low_level_sync_sans_io)
{
try {
result<std::set<std::string>> resp;
char const* wire = "~6\r\n+orange\r\n+apple\r\n+one\r\n+two\r\n+three\r\n+orange\r\n";
deserialize(wire, adapt2(resp));
deserialize(resp3_set, adapt2(resp));
for (auto const& e : resp.value())
std::cout << e << std::endl;
@@ -48,61 +53,6 @@ BOOST_AUTO_TEST_CASE(low_level_sync_sans_io)
}
}
BOOST_AUTO_TEST_CASE(config_to_hello)
{
config cfg;
cfg.clientname = "";
request req;
push_hello(cfg, req);
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_with_select)
{
config cfg;
cfg.clientname = "";
cfg.database_index = 10;
request req;
push_hello(cfg, req);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_cmd_clientname)
{
config cfg;
request req;
push_hello(cfg, req);
std::string_view const
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_cmd_auth)
{
config cfg;
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
request req;
push_hello(cfg, req);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(issue_210_empty_set)
{
try {
@@ -239,163 +189,127 @@ BOOST_AUTO_TEST_CASE(issue_233_optional_array_with_null)
}
}
//===========================================================================
// Multiplexer
std::ostream& operator<<(std::ostream& os, node const& nd)
BOOST_AUTO_TEST_CASE(read_buffer_prepare_error)
{
os << to_string(nd.data_type) << "\n"
<< nd.aggregate_size << "\n"
<< nd.depth << "\n"
<< nd.value;
using boost::redis::detail::read_buffer;
return os;
read_buffer buf;
// Usual case, max size is bigger then requested size.
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
buf.commit(10);
// Corner case, max size is equal to the requested size.
buf.set_config({10, 20});
ec = buf.prepare();
BOOST_TEST(!ec);
buf.commit(10);
buf.consume(20);
auto const tmp = buf;
// Error case, max size is smaller to the requested size.
buf.set_config({10, 9});
ec = buf.prepare();
BOOST_TEST(ec == error_code{boost::redis::error::exceeds_maximum_read_buffer_size});
// Check that an error call has no side effects.
auto const res = buf == tmp;
BOOST_TEST(res);
}
BOOST_AUTO_TEST_CASE(multiplexer_push)
BOOST_AUTO_TEST_CASE(read_buffer_prepare_consume_only_committed_data)
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
using boost::redis::detail::read_buffer;
mpx.get_read_buffer() = ">2\r\n+one\r\n+two\r\n";
read_buffer buf;
boost::system::error_code ec;
auto const ret = mpx.consume_next(ec);
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
BOOST_TEST(ret.first.value());
BOOST_CHECK_EQUAL(ret.second, 16u);
auto res = buf.consume(5);
// TODO: Provide operator << for generic_response so we can compare
// the whole vector.
BOOST_CHECK_EQUAL(resp.value().size(), 3u);
BOOST_CHECK_EQUAL(resp.value().at(1).value, "one");
BOOST_CHECK_EQUAL(resp.value().at(2).value, "two");
// No data has been committed yet so nothing can be consummed.
BOOST_CHECK_EQUAL(res.consumed, 0u);
for (auto const& e : resp.value())
std::cout << e << std::endl;
// If nothing was consumed, nothing got rotated.
BOOST_CHECK_EQUAL(res.rotated, 0u);
buf.commit(10);
res = buf.consume(5);
// All five bytes should have been consumed.
BOOST_CHECK_EQUAL(res.consumed, 5u);
// We added a total of 10 bytes and consumed 5, that means, 5 were
// rotated.
BOOST_CHECK_EQUAL(res.rotated, 5u);
res = buf.consume(7);
// Only the remaining five bytes can be consumed
BOOST_CHECK_EQUAL(res.consumed, 5u);
// No bytes to rotated.
BOOST_CHECK_EQUAL(res.rotated, 0u);
}
BOOST_AUTO_TEST_CASE(multiplexer_push_needs_more)
BOOST_AUTO_TEST_CASE(read_buffer_check_buffer_size)
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
using boost::redis::detail::read_buffer;
// Only part of the message.
mpx.get_read_buffer() = ">2\r\n+one\r";
read_buffer buf;
boost::system::error_code ec;
auto ret = mpx.consume_next(ec);
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
BOOST_TEST(!ret.first.has_value());
mpx.get_read_buffer().append("\n+two\r\n");
ret = mpx.consume_next(ec);
BOOST_TEST(ret.first.value());
BOOST_CHECK_EQUAL(ret.second, 16u);
// TODO: Provide operator << for generic_response so we can compare
// the whole vector.
BOOST_CHECK_EQUAL(resp.value().size(), 3u);
BOOST_CHECK_EQUAL(resp.value().at(1).value, "one");
BOOST_CHECK_EQUAL(resp.value().at(2).value, "two");
BOOST_CHECK_EQUAL(buf.get_prepared().size(), 10u);
}
struct test_item {
request req;
generic_response resp;
std::shared_ptr<multiplexer::elem> elem_ptr;
bool done = false;
test_item(bool cmd_with_response = true)
{
// The exact command is irrelevant because it is not being sent
// to Redis.
req.push(cmd_with_response ? "PING" : "SUBSCRIBE", "cmd-arg");
elem_ptr = std::make_shared<multiplexer::elem>(req, any_adapter(resp).impl_.adapt_fn);
elem_ptr->set_done_callback([this]() {
done = true;
});
}
};
BOOST_AUTO_TEST_CASE(multiplexer_pipeline)
BOOST_AUTO_TEST_CASE(check_counter_adapter)
{
test_item item1{};
test_item item2{false};
test_item item3{};
using boost::redis::any_adapter;
using boost::redis::resp3::parse;
using boost::redis::resp3::parser;
using boost::redis::resp3::node_view;
using boost::system::error_code;
// Add some requests to the multiplexer.
multiplexer mpx;
mpx.add(item1.elem_ptr);
mpx.add(item3.elem_ptr);
mpx.add(item2.elem_ptr);
int init = 0;
int node = 0;
int done = 0;
// These requests haven't been written yet so their statuses should
// be "waiting.".
BOOST_TEST(item1.elem_ptr->is_waiting());
BOOST_TEST(item2.elem_ptr->is_waiting());
BOOST_TEST(item3.elem_ptr->is_waiting());
auto counter_adapter = [&](any_adapter::parse_event ev, node_view const&, error_code&) mutable {
switch (ev) {
case any_adapter::parse_event::init: init++; break;
case any_adapter::parse_event::node: node++; break;
case any_adapter::parse_event::done: done++; break;
}
};
// There are three requests to coalesce, a second call should do
// nothing.
BOOST_CHECK_EQUAL(mpx.prepare_write(), 3u);
BOOST_CHECK_EQUAL(mpx.prepare_write(), 0u);
any_adapter wrapped{any_adapter::impl_t{counter_adapter}};
// After coalescing the requests for writing their statuses should
// be changed to "staged".
BOOST_TEST(item1.elem_ptr->is_staged());
BOOST_TEST(item2.elem_ptr->is_staged());
BOOST_TEST(item3.elem_ptr->is_staged());
error_code ec;
parser p;
// There are no waiting requests to cancel since they are all
// staged.
BOOST_CHECK_EQUAL(mpx.cancel_waiting(), 0u);
auto const ret1 = parse(p, RESP3_SET_PART1, wrapped, ec);
auto const ret2 = parse(p, RESP3_SET_PART1 RESP3_SET_PART2, wrapped, ec);
auto const ret3 = parse(p, RESP3_SET_PART1 RESP3_SET_PART2 RESP3_SET_PART3, wrapped, ec);
auto const ret4 = parse(
p,
RESP3_SET_PART1 RESP3_SET_PART2 RESP3_SET_PART3 RESP3_SET_PART4,
wrapped,
ec);
// Since the requests haven't been sent (written) the done
// callback should not have been called yet.
BOOST_TEST(!item1.done);
BOOST_TEST(!item2.done);
BOOST_TEST(!item3.done);
BOOST_TEST(!ret1);
BOOST_TEST(!ret2);
BOOST_TEST(!ret3);
BOOST_TEST(ret4);
// The commit_write call informs the multiplexer the payload was
// sent (e.g. written to the socket). This step releases requests
// that has no response.
BOOST_CHECK_EQUAL(mpx.commit_write(), 1u);
// The staged status should now have changed to written.
BOOST_TEST(item1.elem_ptr->is_written());
BOOST_TEST(item2.elem_ptr->is_done());
BOOST_TEST(item3.elem_ptr->is_written());
// The done status should still be unchanged on requests that
// expect a response.
BOOST_TEST(!item1.done);
BOOST_TEST(item2.done);
BOOST_TEST(!item3.done);
// Simulates a socket read by putting some data in the read buffer.
mpx.get_read_buffer().append("+one\r\n");
// Consumes the next message in the read buffer.
boost::system::error_code ec;
auto const ret = mpx.consume_next(ec);
// The read operation should have been successfull.
BOOST_TEST(ret.first.has_value());
BOOST_TEST(ret.second != 0u);
// The read buffer should also be empty now
BOOST_TEST(mpx.get_read_buffer().empty());
// The last request still did not get a response.
BOOST_TEST(item1.done);
BOOST_TEST(item2.done);
BOOST_TEST(!item3.done);
// TODO: Check the first request was removed from the queue.
BOOST_CHECK_EQUAL(init, 1);
BOOST_CHECK_EQUAL(node, 7);
BOOST_CHECK_EQUAL(done, 1);
}

1012
test/test_multiplexer.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,13 +5,22 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "sansio_utils.hpp"
#include <chrono>
#include <string_view>
namespace net = boost::asio;
namespace redis = boost::redis;
using boost::system::error_code;
@@ -19,215 +28,479 @@ using net::cancellation_type_t;
using redis::detail::reader_fsm;
using redis::detail::multiplexer;
using redis::generic_response;
using redis::any_adapter;
using redis::config;
using redis::detail::connection_state;
using action = redis::detail::reader_fsm::action;
using redis::logger;
using namespace std::chrono_literals;
// Operators
static const char* to_string(action::type type)
{
switch (type) {
case action::type::read_some: return "action::type::read_some";
case action::type::notify_push_receiver: return "action::type::notify_push_receiver";
case action::type::done: return "action::type::done";
default: return "<unknown action::type>";
}
}
namespace boost::redis::detail {
extern auto to_string(reader_fsm::action::type t) noexcept -> char const*;
std::ostream& operator<<(std::ostream& os, action::type type) { return os << to_string(type); }
std::ostream& operator<<(std::ostream& os, reader_fsm::action::type t)
bool operator==(const action& lhs, const action& rhs) noexcept
{
os << to_string(t);
return os;
if (lhs.get_type() != rhs.get_type())
return false;
switch (lhs.get_type()) {
case action::type::done: return lhs.error() == rhs.error();
case action::type::read_some: return lhs.timeout() == rhs.timeout();
case action::type::notify_push_receiver: return lhs.push_size() == rhs.push_size();
default: BOOST_ASSERT(false); return false;
}
}
std::ostream& operator<<(std::ostream& os, const action& act)
{
auto t = act.get_type();
os << "action{ .type=" << t;
switch (t) {
case action::type::done: os << ", .error=" << act.error(); break;
case action::type::read_some:
os << ", .timeout=" << to_milliseconds(act.timeout()) << "ms";
break;
case action::type::notify_push_receiver: os << ", .push_size=" << act.push_size(); break;
default: BOOST_ASSERT(false);
}
return os << " }";
}
} // namespace boost::redis::detail
// Operators
namespace {
// Copy data into the multiplexer with the following steps
//
// 1. get_read_buffer
// 2. Copy data in the buffer from 2.
//
// This is used in the reader_fsm tests.
void copy_to(multiplexer& mpx, std::string_view data)
{
auto const buffer = mpx.get_prepared_read_buffer();
BOOST_ASSERT(buffer.size() >= data.size());
std::copy(data.cbegin(), data.cend(), buffer.begin());
}
struct fixture : redis::detail::log_fixture {
connection_state st{{make_logger()}};
generic_response resp;
fixture()
{
st.mpx.set_receive_adapter(any_adapter{resp});
st.cfg.health_check_interval = 3s;
}
};
void test_push()
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
reader_fsm fsm{mpx};
error_code ec;
action act;
fixture fix;
reader_fsm fsm;
// Initiate
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::setup_cancellation);
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
mpx.get_read_buffer().append(">1\r\n+msg1\r\n");
mpx.get_read_buffer().append(">1\r\n+msg2 \r\n");
mpx.get_read_buffer().append(">1\r\n+msg3 \r\n");
auto const bytes_read = mpx.get_read_buffer().size();
std::string const payload =
">1\r\n+msg1\r\n"
">1\r\n+msg2 \r\n"
">1\r\n+msg3 \r\n";
copy_to(fix.st.mpx, payload);
// Deliver the 1st push
act = fsm.resume(bytes_read, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver);
BOOST_TEST_EQ(act.push_size_, 11u);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::notify_push_receiver(11u));
// Deliver the 2st push
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver);
BOOST_TEST_EQ(act.push_size_, 12u);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::notify_push_receiver(12u));
// Deliver the 3rd push
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver);
BOOST_TEST_EQ(act.push_size_, 13u);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::notify_push_receiver(13u));
// All pushes were delivered so the fsm should demand more data
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 36 bytes read"},
{logger::level::debug, "Reader task: issuing read" },
});
}
void test_read_needs_more()
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
reader_fsm fsm{mpx};
error_code ec;
action act;
fixture fix;
reader_fsm fsm;
// Initiate
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::setup_cancellation);
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// Split the incoming message in three random parts and deliver
// them to the reader individually.
std::string const msg[] = {">3\r", "\n+msg1\r\n+ms", "g2\r\n+msg3\r\n"};
// Passes the first part to the fsm.
mpx.get_read_buffer().append(msg[0]);
act = fsm.resume(msg[0].size(), ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::needs_more);
BOOST_TEST_EQ(act.ec_, error_code());
copy_to(fix.st.mpx, msg[0]);
act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// Passes the second part to the fsm.
mpx.get_read_buffer().append(msg[1]);
act = fsm.resume(msg[1].size(), ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::needs_more);
BOOST_TEST_EQ(act.ec_, error_code());
copy_to(fix.st.mpx, msg[1]);
act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type_t::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.
mpx.get_read_buffer().append(msg[2]);
act = fsm.resume(msg[2].size(), ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver);
BOOST_TEST_EQ(act.push_size_, msg[0].size() + msg[1].size() + msg[2].size());
BOOST_TEST_EQ(act.ec_, error_code());
copy_to(fix.st.mpx, msg[2]);
act = fsm.resume(fix.st, msg[2].size(), error_code(), cancellation_type_t::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(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 3 bytes read" },
{logger::level::debug, "Reader task: incomplete message received"},
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 11 bytes read" },
{logger::level::debug, "Reader task: incomplete message received"},
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 11 bytes read" },
{logger::level::debug, "Reader task: issuing read" },
});
}
void test_health_checks_disabled()
{
fixture fix;
reader_fsm fsm;
fix.st.cfg.health_check_interval = 0s;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::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
constexpr std::string_view msg[] = {">3\r\n+msg1\r\n+ms", "g2\r\n+msg3\r\n"};
// 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);
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);
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);
BOOST_TEST_EQ(act, action::read_some(0s));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 14 bytes read" },
{logger::level::debug, "Reader task: incomplete message received"},
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 11 bytes read" },
{logger::level::debug, "Reader task: issuing read" },
});
}
void test_read_error()
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
reader_fsm fsm{mpx};
error_code ec;
action act;
fixture fix;
reader_fsm fsm;
// Initiate
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::setup_cancellation);
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
mpx.get_read_buffer().append(">1\r\n+msg1\r\n");
auto const bytes_read = mpx.get_read_buffer().size();
std::string const payload = ">1\r\n+msg1\r\n";
copy_to(fix.st.mpx, payload);
// Deliver the data
act = fsm.resume(bytes_read, {net::error::operation_aborted}, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::cancel_run);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, payload.size(), {redis::error::empty_field}, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code{redis::error::empty_field});
// Finish
act = fsm.resume(bytes_read, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::done);
BOOST_TEST_EQ(act.ec_, error_code{net::error::operation_aborted});
// Check logging
fix.check_log({
// clang-format off
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 11 bytes read, error: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
// A timeout in a read means that the connection is unhealthy (i.e. a PING timed out)
void test_read_timeout()
{
fixture fix;
reader_fsm fsm;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// Timeout
act = fsm.resume(fix.st, 0, {net::error::operation_aborted}, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code{redis::error::pong_timeout});
// Check logging
fix.check_log({
// clang-format off
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 0 bytes read, error: Pong timeout. [boost.redis:19]"},
// clang-format on
});
}
void test_parse_error()
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
reader_fsm fsm{mpx};
error_code ec;
action act;
fixture fix;
reader_fsm fsm;
// Initiate
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::setup_cancellation);
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
mpx.get_read_buffer().append(">a\r\n");
auto const bytes_read = mpx.get_read_buffer().size();
std::string const payload = ">a\r\n";
copy_to(fix.st.mpx, payload);
// Deliver the data
act = fsm.resume(bytes_read, {}, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::cancel_run);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code{redis::error::not_a_number});
// Finish
act = fsm.resume(bytes_read, {}, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::done);
BOOST_TEST_EQ(act.ec_, error_code{redis::error::not_a_number});
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read"},
{logger::level::debug, "Reader task: 4 bytes read"},
{logger::level::debug,
"Reader task: error processing message: Can't convert string to number (maybe forgot to "
"upgrade to RESP3?). [boost.redis:2]" },
});
}
void test_push_deliver_error()
{
multiplexer mpx;
generic_response resp;
mpx.set_receive_response(resp);
reader_fsm fsm{mpx};
error_code ec;
action act;
fixture fix;
reader_fsm fsm;
// Initiate
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::setup_cancellation);
act = fsm.resume(0, ec, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::append_some);
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
mpx.get_read_buffer().append(">1\r\n+msg1\r\n");
auto const bytes_read = mpx.get_read_buffer().size();
std::string const payload = ">1\r\n+msg1\r\n";
copy_to(fix.st.mpx, payload);
// Deliver the data
act = fsm.resume(bytes_read, {}, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver);
BOOST_TEST_EQ(act.ec_, error_code());
act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none);
BOOST_TEST_EQ(act, action::notify_push_receiver(11u));
// Resumes from notifying a push with an error.
act = fsm.resume(bytes_read, net::error::operation_aborted, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::cancel_run);
act = fsm.resume(fix.st, 0, redis::error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code{redis::error::empty_field});
// Finish
act = fsm.resume(0, {}, cancellation_type_t::none);
BOOST_TEST_EQ(act.type_, action::type::done);
BOOST_TEST_EQ(act.ec_, error_code{net::error::operation_aborted});
// Check logging
fix.check_log({
// clang-format off
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 11 bytes read" },
{logger::level::debug, "Reader task: error notifying push receiver: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
void test_max_read_buffer_size()
{
fixture fix;
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;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, error_code(redis::error::exceeds_maximum_read_buffer_size));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 4 bytes read" },
{logger::level::debug, "Reader task: incomplete message received"},
{logger::level::debug,
"Reader task: error in prepare_read: Reading data from the socket would exceed the maximum "
"size allowed of the read buffer. [boost.redis:26]" },
});
}
// Cancellations
void test_cancel_read()
{
fixture fix;
reader_fsm fsm;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The read was cancelled (maybe after delivering some bytes)
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));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: cancelled (1)"},
});
}
void test_cancel_read_edge()
{
fixture fix;
reader_fsm fsm;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::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));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: cancelled (1)"},
});
}
void test_cancel_push_delivery()
{
fixture fix;
reader_fsm fsm;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
constexpr std::string_view payload =
">1\r\n+msg1\r\n"
">1\r\n+msg2 \r\n";
copy_to(fix.st.mpx, payload);
// Deliver the 1st push
act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::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));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 23 bytes read"},
{logger::level::debug, "Reader task: cancelled (2)"},
});
}
void test_cancel_push_delivery_edge()
{
fixture fix;
reader_fsm fsm;
// Initiate
auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, action::read_some(6s));
// The fsm is asking for data.
constexpr std::string_view payload =
">1\r\n+msg1\r\n"
">1\r\n+msg2 \r\n";
copy_to(fix.st.mpx, payload);
// Deliver the 1st push
act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::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));
// Check logging
fix.check_log({
{logger::level::debug, "Reader task: issuing read" },
{logger::level::debug, "Reader task: 23 bytes read"},
{logger::level::debug, "Reader task: cancelled (2)"},
});
}
} // namespace
int main()
{
test_push_deliver_error();
test_read_needs_more();
test_push();
test_read_needs_more();
test_health_checks_disabled();
test_read_error();
test_read_timeout();
test_parse_error();
test_push_deliver_error();
test_max_read_buffer_size();
test_cancel_read();
test_cancel_read_edge();
test_cancel_push_delivery();
test_cancel_push_delivery_edge();
return boost::report_errors();
}

572
test/test_run_fsm.cpp Normal file
View File

@@ -0,0 +1,572 @@
//
// 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 <boost/redis/config.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/run_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/local/basic_endpoint.hpp> // for BOOST_ASIO_HAS_LOCAL_SOCKETS
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "sansio_utils.hpp"
#include <ostream>
#include <string_view>
using namespace boost::redis;
namespace asio = boost::asio;
using detail::run_fsm;
using detail::multiplexer;
using detail::run_action_type;
using detail::run_action;
using boost::system::error_code;
using boost::asio::cancellation_type_t;
using namespace std::chrono_literals;
// Operators
static const char* to_string(run_action_type value)
{
switch (value) {
case run_action_type::done: return "run_action_type::done";
case run_action_type::immediate: return "run_action_type::immediate";
case run_action_type::connect: return "run_action_type::connect";
case run_action_type::parallel_group: return "run_action_type::parallel_group";
case run_action_type::cancel_receive: return "run_action_type::cancel_receive";
case run_action_type::wait_for_reconnection: return "run_action_type::wait_for_reconnection";
default: return "<unknown run_action_type>";
}
}
namespace boost::redis::detail {
std::ostream& operator<<(std::ostream& os, run_action_type type)
{
os << to_string(type);
return os;
}
bool operator==(const run_action& lhs, const run_action& rhs) noexcept
{
return lhs.type == rhs.type && lhs.ec == rhs.ec;
}
std::ostream& operator<<(std::ostream& os, const run_action& act)
{
os << "run_action{ .type=" << act.type;
if (act.type == run_action_type::done)
os << ", .error=" << act.ec;
return os << " }";
}
} // namespace boost::redis::detail
namespace {
struct fixture : detail::log_fixture {
detail::connection_state st;
run_fsm fsm;
static config default_config()
{
config res;
res.use_setup = true;
res.setup.clear();
return res;
}
fixture(config&& cfg = default_config())
: st{{make_logger()}, std::move(cfg)}
{ }
};
config config_no_reconnect()
{
auto res = fixture::default_config();
res.reconnect_wait_interval = 0s;
return res;
}
// 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)};
// Launching the operation fails immediately
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::immediate);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::unix_sockets_unsupported));
// Log
fix.check_log({
{logger::level::err,
"Invalid configuration: The configuration specified a UNIX socket address, but UNIX sockets "
"are not supported by the system. [boost.redis:24]"},
});
}
#endif
void test_config_error_unix_ssl()
{
// Setup
config cfg;
cfg.use_ssl = true;
cfg.unix_socket = "/var/sock";
fixture fix{std::move(cfg)};
// Launching the operation fails immediately
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::immediate);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::unix_sockets_ssl_unsupported));
// Log
fix.check_log({
{logger::level::err,
"Invalid configuration: The configuration specified UNIX sockets with SSL, which is not "
"supported. [boost.redis:25]"},
});
}
// An error in connect with reconnection enabled triggers a reconnection
void test_connect_error()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// An error in connect without reconnection enabled makes the operation finish
void test_connect_error_no_reconnect()
{
// Setup
fixture fix{config_no_reconnect()};
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in connect makes the operation finish even with reconnection enabled
void test_connect_cancel()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (1)"}
});
}
// Same, but only the cancellation is set
void test_connect_cancel_edge()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (1)"}
});
}
// An error in the parallel group triggers a reconnection
// (the parallel group always exits with an error)
void test_parallel_group_error()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// An error in the parallel group makes the operation exit if reconnection is disabled
void test_parallel_group_error_no_reconnect()
{
// Setup
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in the parallel group makes it exit, even if reconnection is enabled.
// Parallel group tasks always exit with an error, so there is no edge case here
void test_parallel_group_cancel()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
void test_parallel_group_cancel_no_reconnect()
{
// Setup
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
// If the reconnection wait gets cancelled, we exit
void test_wait_cancel()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (3)"}
});
}
void test_wait_cancel_edge()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (3)"}
});
}
void test_several_reconnections()
{
// Setup
fixture fix;
// Run the operation. Connect errors and we sleep
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::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);
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));
// The cancellation was logged
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
// Setup and ping requests are only composed once at startup
void test_setup_ping_requests()
{
// Setup
config cfg;
cfg.health_check_id = "some_value";
cfg.username = "foo";
cfg.password = "bar";
cfg.clientname = "";
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// At this point, the requests are set up
const std::string_view expected_ping = "*2\r\n$4\r\nPING\r\n$10\r\nsome_value\r\n";
const std::string_view
expected_setup = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
// Reconnect
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// The requests haven't been modified
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
}
// We correctly send and log the setup request
void test_setup_request_success()
{
// Setup
fixture fix;
fix.st.cfg.setup.clear();
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// At this point, the setup request should be already queued. Simulate the writer
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 1u);
BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size()));
// Simulate a successful read
read(fix.st.mpx, "+OK\r\n");
error_code ec;
auto res = fix.st.mpx.consume(ec);
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST(res.first == detail::consume_result::got_response);
// Check log
fix.check_log({
{logger::level::info, "Setup request execution: success"}
});
}
// We don't send empty setup requests
void test_setup_request_empty()
{
// Setup
fixture fix;
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Nothing was added to the multiplexer
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 0u);
// Check log
fix.check_log({});
}
// A server error would cause the reader to exit
void test_setup_request_server_error()
{
// Setup
fixture fix;
fix.st.setup_diagnostic = "leftover"; // simulate a leftover from previous runs
fix.st.cfg.setup.clear();
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);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// At this point, the setup request should be already queued. Simulate the writer
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 1u);
BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size()));
// Simulate a successful read
read(fix.st.mpx, "-ERR: wrong command\r\n");
error_code ec;
auto res = fix.st.mpx.consume(ec);
BOOST_TEST_EQ(ec, error::resp3_hello);
BOOST_TEST(res.first == detail::consume_result::got_response);
// Check log
fix.check_log({
{logger::level::info,
"Setup request execution: The server response to the setup request sent during connection "
"establishment contains an error. [boost.redis:23] (ERR: wrong command)"}
});
}
} // namespace
int main()
{
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
test_config_error_unix();
#endif
test_config_error_unix_ssl();
test_connect_error();
test_connect_error_no_reconnect();
test_connect_cancel();
test_connect_cancel_edge();
test_parallel_group_error();
test_parallel_group_error_no_reconnect();
test_parallel_group_cancel();
test_parallel_group_cancel_no_reconnect();
test_wait_cancel();
test_wait_cancel_edge();
test_several_reconnections();
test_setup_ping_requests();
test_setup_request_success();
test_setup_request_empty();
test_setup_request_server_error();
return boost::report_errors();
}

View File

@@ -0,0 +1,196 @@
//
// 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 <boost/redis/adapter/result.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/setup_request_utils.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/result.hpp>
namespace asio = boost::asio;
namespace redis = boost::redis;
using redis::detail::compose_setup_request;
using boost::system::error_code;
namespace {
void test_compose_setup()
{
redis::config cfg;
cfg.clientname = "";
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_select()
{
redis::config cfg;
cfg.clientname = "";
cfg.database_index = 10;
compose_setup_request(cfg);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_clientname()
{
redis::config cfg;
compose_setup_request(cfg);
std::string_view const
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_auth()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
compose_setup_request(cfg);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_auth_empty_password()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
compose_setup_request(cfg);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_auth_setname()
{
redis::config cfg;
cfg.clientname = "mytest";
cfg.username = "foo";
cfg.password = "bar";
compose_setup_request(cfg);
std::string_view const expected =
"*7\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$7\r\nSETNAME\r\n$"
"6\r\nmytest\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_compose_setup_use_setup()
{
redis::config cfg;
cfg.clientname = "mytest";
cfg.username = "foo";
cfg.password = "bar";
cfg.database_index = 4;
cfg.use_setup = true;
cfg.setup.push("SELECT", 8);
compose_setup_request(cfg);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
// Regression check: we set the priority flag
void test_compose_setup_use_setup_no_hello()
{
redis::config cfg;
cfg.use_setup = true;
cfg.setup.clear();
cfg.setup.push("SELECT", 8);
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
// Regression check: we set the relevant cancellation flags in the request
void test_compose_setup_use_setup_flags()
{
redis::config cfg;
cfg.use_setup = true;
cfg.setup.clear();
cfg.setup.push("SELECT", 8);
cfg.setup.get_config().cancel_if_unresponded = false;
cfg.setup.get_config().cancel_on_connection_lost = false;
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
} // namespace
int main()
{
test_compose_setup();
test_compose_setup_select();
test_compose_setup_clientname();
test_compose_setup_auth();
test_compose_setup_auth_empty_password();
test_compose_setup_auth_setname();
test_compose_setup_use_setup();
test_compose_setup_use_setup_no_hello();
test_compose_setup_use_setup_flags();
return boost::report_errors();
}

View File

@@ -81,6 +81,9 @@ void test_reconnection()
cfg.reconnect_wait_interval = 10ms; // make the test run faster
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;
@@ -103,12 +106,6 @@ void test_reconnection()
auto quit_callback = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
// If a request is issued immediately after QUIT, the request sometimes
// fails, probably due to a race condition. This dispatches any pending
// handlers, triggering the reconnection process.
// TODO: this should not be required.
ioc.poll();
conn.async_exec(ping_request, ignore, ping_callback);
};

534
test/test_writer_fsm.cpp Normal file
View File

@@ -0,0 +1,534 @@
//
// 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 <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/writer_fsm.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "sansio_utils.hpp"
#include <chrono>
#include <memory>
#include <ostream>
#include <string_view>
using namespace boost::redis;
namespace asio = boost::asio;
using detail::writer_fsm;
using detail::multiplexer;
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 namespace std::chrono_literals;
// Operators
static const char* to_string(writer_action_type value)
{
switch (value) {
case writer_action_type::done: return "writer_action_type::done";
case writer_action_type::write_some: return "writer_action_type::write";
case writer_action_type::wait: return "writer_action_type::wait";
default: return "<unknown writer_action_type>";
}
}
namespace boost::redis::detail {
std::ostream& operator<<(std::ostream& os, writer_action_type type)
{
os << to_string(type);
return os;
}
bool operator==(const writer_action& lhs, const writer_action& rhs) noexcept
{
if (lhs.type() != rhs.type())
return false;
switch (lhs.type()) {
case writer_action_type::done: return lhs.error() == rhs.error();
case writer_action_type::write_some:
case writer_action_type::wait: return lhs.timeout() == rhs.timeout();
default: BOOST_ASSERT(false);
}
return false;
}
std::ostream& operator<<(std::ostream& os, const writer_action& act)
{
auto t = act.type();
os << "writer_action{ .type=" << t;
switch (t) {
case writer_action_type::done: os << ", .error=" << act.error(); break;
case writer_action_type::write_some:
case writer_action_type::wait:
os << ", .timeout=" << to_milliseconds(act.timeout()) << "ms";
break;
default: BOOST_ASSERT(false);
}
return os << " }";
}
} // namespace boost::redis::detail
namespace {
// A helper to create a request and its associated elem
struct test_elem {
request req;
bool done{false};
std::shared_ptr<multiplexer::elem> elm;
test_elem()
{
// Empty requests are not valid. The request needs to be populated before creating the element
req.push("get", "mykey");
elm = std::make_shared<multiplexer::elem>(req, any_adapter{});
elm->set_done_callback([this] {
done = true;
});
}
};
struct fixture : detail::log_fixture {
connection_state st{{make_logger()}};
writer_fsm fsm;
fixture()
{
st.ping_req.push("PING", "ping_msg"); // would be set up by the runner
st.cfg.health_check_interval = 4s;
}
};
// A single request is written, then we wait and repeat
void test_single_request()
{
// Setup
fixture fix;
test_elem item1, item2;
// A request arrives before the writer starts
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
BOOST_TEST(item1.elm->is_written());
// Another request arrives
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
BOOST_TEST(item2.elm->is_written());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 24 bytes written."},
{logger::level::debug, "Writer task: 24 bytes written."},
});
}
// If a request arrives while we're performing a write, we don't get back to sleep
void test_request_arrives_while_writing()
{
// Setup
fixture fix;
test_elem item1, item2;
// A request arrives before the writer starts
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);
BOOST_TEST_EQ(act, writer_action::write_some(4s));
BOOST_TEST(item1.elm->is_staged());
// While the write is outstanding, a new request arrives
fix.st.mpx.add(item2.elm);
// 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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
BOOST_TEST(item2.elm->is_written());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 24 bytes written."},
{logger::level::debug, "Writer task: 24 bytes written."},
});
}
// If there is no request when the writer starts, we wait for it
void test_no_request_at_startup()
{
// 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_t::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);
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);
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."},
});
}
// We correctly handle short writes
void test_short_writes()
{
// Setup
fixture fix;
test_elem item1;
// A request arrives before the writer starts
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);
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);
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);
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
BOOST_TEST(item1.elm->is_written());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 2 bytes written." },
{logger::level::debug, "Writer task: 5 bytes written." },
{logger::level::debug, "Writer task: 0 bytes written." },
{logger::level::debug, "Writer task: 17 bytes written."},
});
}
// If no data arrives during the health check interval, a ping is written
void test_ping()
{
// Setup
fixture fix;
error_code ec;
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);
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
// Simulate a successful response to the PING
constexpr std::string_view ping_response = "$8\r\nping_msg\r\n";
read(fix.st.mpx, ping_response);
auto res = fix.st.mpx.consume(ec);
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST(res.first == consume_result::got_response);
BOOST_TEST_EQ(res.second, ping_response.size());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 28 bytes written."},
});
}
// Disabled health checks don't cause trouble
void test_health_checks_disabled()
{
// Setup
fixture fix;
test_elem item;
fix.st.cfg.health_check_interval = 0s;
// A request arrives before the writer starts
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(0s));
BOOST_TEST(item.elm->is_written());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 24 bytes written."},
});
}
// If the server answers with an error in PING, we log it and produce an error
void test_ping_error()
{
// Setup
fixture fix;
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);
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);
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);
BOOST_TEST_EQ(act, writer_action::wait(4s));
// Simulate an error response to the PING
constexpr std::string_view ping_response = "-ERR: bad command\r\n";
read(fix.st.mpx, ping_response);
auto res = fix.st.mpx.consume(ec);
BOOST_TEST_EQ(ec, error::resp3_simple_error);
BOOST_TEST(res.first == consume_result::got_response);
BOOST_TEST_EQ(res.second, ping_response.size());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 28 bytes written." },
{logger::level::info, "Health checker: server answered ping with an error: ERR: bad command"},
});
}
// A write error makes the writer exit
void test_write_error()
{
// Setup
fixture fix;
test_elem item;
// A request arrives before the writer starts
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);
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);
BOOST_TEST_EQ(act, error_code(error::empty_field));
BOOST_TEST(item.elm->is_staged());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 2 bytes written." },
{logger::level::debug, "Writer task error: Expected field value is empty. [boost.redis:5]"},
});
}
void test_write_timeout()
{
// Setup
fixture fix;
test_elem item;
// A request arrives before the writer starts
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);
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);
BOOST_TEST_EQ(act, error_code(error::write_timeout));
BOOST_TEST(item.elm->is_staged());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 0 bytes written." },
{logger::level::debug,
"Writer task error: Timeout while writing data to the server. [boost.redis:27]"},
});
}
// A write is cancelled
void test_cancel_write()
{
// Setup
fixture fix;
test_elem item;
// A request arrives before the writer starts
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);
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));
BOOST_TEST(item.elm->is_staged());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 2 bytes written."},
{logger::level::debug, "Writer task: cancelled (1)." },
});
}
// A write is cancelled after completing but before the handler is dispatched
void test_cancel_write_edge()
{
// Setup
fixture fix;
test_elem item;
// A request arrives before the writer starts
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);
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));
BOOST_TEST(item.elm->is_written());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: 24 bytes written."},
{logger::level::debug, "Writer task: cancelled (1)." },
});
}
// The wait was cancelled because of per-operation cancellation (rather than a notification)
void test_cancel_wait()
{
// 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_t::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));
BOOST_TEST(item.elm->is_waiting());
// Logs
fix.check_log({
{logger::level::debug, "Writer task: cancelled (2)."},
});
}
} // namespace
int main()
{
test_single_request();
test_request_arrives_while_writing();
test_no_request_at_startup();
test_short_writes();
test_health_checks_disabled();
test_ping();
test_ping_error();
test_write_error();
test_write_timeout();
test_cancel_write();
test_cancel_write_edge();
test_cancel_wait();
return boost::report_errors();
}

View File

@@ -1,6 +1,6 @@
services:
redis:
image: "redis:alpine"
image: ${SERVER_IMAGE}
entrypoint: "/docker/entrypoint.sh"
volumes:
- ./docker:/docker
@@ -9,7 +9,7 @@ services:
- 6379:6379
- 6380:6380
builder:
image: $IMAGE
image: ${BUILDER_IMAGE}
container_name: builder
tty: true
environment: