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:
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
* xref:index.adoc[Introduction]
|
||||
* xref:requests_responses.adoc[]
|
||||
* xref:cancellation.adoc[]
|
||||
* xref:serialization.adoc[]
|
||||
* xref:logging.adoc[]
|
||||
* xref:benchmarks.adoc[]
|
||||
|
||||
79
doc/modules/ROOT/pages/cancellation.adoc
Normal file
79
doc/modules/ROOT/pages/cancellation.adoc
Normal 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);
|
||||
----
|
||||
@@ -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].
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
49
example/cpp20_timeouts.cpp
Normal file
49
example/cpp20_timeouts.cpp
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
97
include/boost/redis/detail/connect_fsm.hpp
Normal file
97
include/boost/redis/detail/connect_fsm.hpp
Normal 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
|
||||
@@ -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
|
||||
34
include/boost/redis/detail/connection_state.hpp
Normal file
34
include/boost/redis/detail/connection_state.hpp
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
69
include/boost/redis/detail/read_buffer.hpp
Normal file
69
include/boost/redis/detail/read_buffer.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
29
include/boost/redis/detail/resp3_type_to_error.hpp
Normal file
29
include/boost/redis/detail/resp3_type_to_error.hpp
Normal 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
|
||||
62
include/boost/redis/detail/run_fsm.hpp
Normal file
62
include/boost/redis/detail/run_fsm.hpp
Normal 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
|
||||
92
include/boost/redis/detail/writer_fsm.hpp
Normal file
92
include/boost/redis/detail/writer_fsm.hpp
Normal 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
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
235
include/boost/redis/impl/connect_fsm.ipp
Normal file
235
include/boost/redis/impl/connect_fsm.ipp
Normal 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
|
||||
@@ -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
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
23
include/boost/redis/impl/is_terminal_cancel.hpp
Normal file
23
include/boost/redis/impl/is_terminal_cancel.hpp
Normal 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
|
||||
99
include/boost/redis/impl/log_utils.hpp
Normal file
99
include/boost/redis/impl/log_utils.hpp
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
80
include/boost/redis/impl/read_buffer.ipp
Normal file
80
include/boost/redis/impl/read_buffer.ipp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
177
include/boost/redis/impl/run_fsm.ipp
Normal file
177
include/boost/redis/impl/run_fsm.ipp
Normal 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
|
||||
59
include/boost/redis/impl/setup_request_utils.hpp
Normal file
59
include/boost/redis/impl/setup_request_utils.hpp
Normal 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
|
||||
128
include/boost/redis/impl/writer_fsm.ipp
Normal file
128
include/boost/redis/impl/writer_fsm.ipp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
75
test/sansio_utils.cpp
Normal 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
55
test/sansio_utils.hpp
Normal 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
|
||||
@@ -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);
|
||||
|
||||
110
test/test_conn_cancel_after.cpp
Normal file
110
test/test_conn_cancel_after.cpp
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
121
test/test_conn_monitor.cpp
Normal 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
112
test/test_conn_move.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
88
test/test_conn_run_cancel.cpp
Normal file
88
test/test_conn_run_cancel.cpp
Normal 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
329
test/test_conn_setup.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
631
test/test_connect_fsm.cpp
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
1012
test/test_multiplexer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
572
test/test_run_fsm.cpp
Normal 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();
|
||||
}
|
||||
196
test/test_setup_request_utils.cpp
Normal file
196
test/test_setup_request_utils.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
534
test/test_writer_fsm.cpp
Normal 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();
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user