mirror of
https://github.com/boostorg/redis.git
synced 2026-01-19 16:52:08 +00:00
Compare commits
64 Commits
parser_eve
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ee72830b | ||
|
|
89e44dc017 | ||
|
|
002b616dd9 | ||
|
|
c11a5194d8 | ||
|
|
bea547481a | ||
|
|
3b07119e54 | ||
|
|
7750a6b126 | ||
|
|
2bbf0090b5 | ||
|
|
02632b31c6 | ||
|
|
6005ebd04a | ||
|
|
755d14a10d | ||
|
|
d9e4b2c720 | ||
|
|
91afb4a279 | ||
|
|
bdd9c327c1 | ||
|
|
00f3ec9b78 | ||
|
|
b365b96228 | ||
|
|
2c1f1c4c50 | ||
|
|
6ff474008f | ||
|
|
bd799aff96 | ||
|
|
c284960549 | ||
|
|
b1420d3d1d | ||
|
|
84fa39918f | ||
|
|
019a080b65 | ||
|
|
53e5ae0cd4 | ||
|
|
1d9f9ab0e3 | ||
|
|
5444e077f9 | ||
|
|
ecd1573257 | ||
|
|
9419c857fd | ||
|
|
b78cf818e0 | ||
|
|
7d959c1039 | ||
|
|
ccb17f89cd | ||
|
|
c9375a44eb | ||
|
|
c88f9f4666 | ||
|
|
682bec618d | ||
|
|
6791e759f9 | ||
|
|
0460b38e14 | ||
|
|
033f6aaa62 | ||
|
|
42411be444 | ||
|
|
6be0d122fb | ||
|
|
2b09ecbd78 | ||
|
|
da09787d29 | ||
|
|
f683e368dd | ||
|
|
28ed27ce72 | ||
|
|
35fa68b926 | ||
|
|
228b31917c | ||
|
|
d3e335942f | ||
|
|
0c159280ba | ||
|
|
1812be87bf | ||
|
|
5771128f2d | ||
|
|
2babb79425 | ||
|
|
a70bdf6574 | ||
|
|
e414b3941a | ||
|
|
beab3f69ed | ||
|
|
f955dc01d2 | ||
|
|
bcf120bd8f | ||
|
|
203e9298ed | ||
|
|
8da18379ba | ||
|
|
40417a13b2 | ||
|
|
74909be47d | ||
|
|
6a1a07f95a | ||
|
|
0cf2441ed2 | ||
|
|
2133ed747b | ||
|
|
1f6c6bd64d | ||
|
|
da2f0101d0 |
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: |
|
||||
|
||||
@@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT)
|
||||
test
|
||||
json
|
||||
endian
|
||||
compat
|
||||
)
|
||||
|
||||
foreach(dep IN LISTS deps)
|
||||
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
56
README.md
56
README.md
@@ -87,43 +87,51 @@ them are:
|
||||
* [Client-side caching](https://redis.io/docs/manual/client-side-caching/).
|
||||
|
||||
The connection class supports server pushes by means of the
|
||||
`connection::async_receive` function, which can be
|
||||
`connection::async_receive2` function, which can be
|
||||
called in the same connection that is being used to execute commands.
|
||||
The coroutine below shows how to use it:
|
||||
The coroutine below shows how to use it
|
||||
|
||||
|
||||
```cpp
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Reconnect to channels.
|
||||
co_await conn->async_exec(req, ignore);
|
||||
|
||||
// Loop reading Redis pushes.
|
||||
for (;;) {
|
||||
error_code ec;
|
||||
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
// Use the response resp in some way and then clear it.
|
||||
...
|
||||
|
||||
consume_one(resp);
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Further reading
|
||||
|
||||
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).
|
||||
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
* xref:index.adoc[Introduction]
|
||||
* xref:requests_responses.adoc[]
|
||||
* xref:cancellation.adoc[]
|
||||
* xref:serialization.adoc[]
|
||||
* xref:logging.adoc[]
|
||||
* xref:sentinel.adoc[]
|
||||
* xref:benchmarks.adoc[]
|
||||
* xref:comparison.adoc[]
|
||||
* xref:examples.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);
|
||||
----
|
||||
@@ -7,6 +7,146 @@
|
||||
|
||||
= Changelog
|
||||
|
||||
== Boost 1.90
|
||||
|
||||
|
||||
* (Pull request https://github.com/boostorg/redis/pull/310[310])
|
||||
Improves the per-operation support in `async_exec()`. Requests can now
|
||||
be cancelled at any point, and cancellations don't interfere with other
|
||||
requests anyhow. In previous versions, a cancellation could cause
|
||||
`async_run()` to be cancelled, making cancellations unpredictable.
|
||||
* (Issue https://github.com/boostorg/redis/issues/226[226])
|
||||
Added support for the `asio::cancel_after` and `asio::cancel_at`
|
||||
completion tokens in `async_exec()` and `async_run()`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/319[319])
|
||||
Added support for per-operation cancellation in `async_run()`.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/329[329]
|
||||
and https://github.com/boostorg/redis/pull/334[334])
|
||||
The `cancel_on_connection_lost` and `cancel_if_not_connected`
|
||||
flags in `request::config` have been deprecated, and will be removed
|
||||
in subsequent releases. To limit the time span that `async_exec`
|
||||
might take, use `asio::cancel_after`, instead.
|
||||
`cancel_on_connection_lost` default has been changed to `false`.
|
||||
This shouldn't cause much impact, since `cancel_on_connection_lost`
|
||||
is not a reliable way to limit the time span that `async_exec` might take.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/321[321])
|
||||
Calling `cancel` with `operation::resolve`, `operation::connect`,
|
||||
`operation::ssl_handshake`, `operation::reconnection` and
|
||||
`operation::health_check` is now deprecated.
|
||||
These enumerators will be removed in subsequent releases.
|
||||
Users should employ `cancel(operation::run)`, instead.
|
||||
* (Issue github.com/boostorg/redis/issues/302[302] and
|
||||
pull request https://github.com/boostorg/redis/pull/303[303])
|
||||
Added support for custom setup requests using `config::setup`
|
||||
and `config::use_setup`. When setting these fields, users can
|
||||
replace the library-generated `HELLO` request by any other arbitrary request.
|
||||
This request is executed every time a new physical connection with the server
|
||||
is established. This feature can be used to interact with systems that don't
|
||||
support `HELLO`, to handle authentication and to connect to replicas.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/305[305])
|
||||
`request::config::hello_with_priority` and `request::has_hello_priority()` have
|
||||
been deprecated and will be removed in subsequent releases.
|
||||
This flag is not well specified and should only be used by the library.
|
||||
If you need to execute a request before any other, use `config::setup`, instead.
|
||||
* (Issue https://github.com/boostorg/redis/issues/296[296])
|
||||
Valkey long-term support: we guarantee Valkey compatibility
|
||||
starting with this release. Previous releases may also work,
|
||||
but have not been tested with this database system.
|
||||
* (Issue https://github.com/boostorg/redis/issues/341[341])
|
||||
Adds a `request::append()` function, to concatenate request objects.
|
||||
* (Issue https://github.com/boostorg/redis/issues/104[104])
|
||||
The health checker algorithm has been redesigned to avoid
|
||||
false positives under heavy loads. `PING` commands are now
|
||||
only issued when the connection is idle, instead of periodically.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/283[283])
|
||||
Added `config::read_buffer_append_size`, which allows to control
|
||||
the expansion of the connection's read buffer.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/311[311])
|
||||
Added `usage::bytes_rotated`, which measures data copying when
|
||||
reading and parsing data from the server.
|
||||
* (Issue https://github.com/boostorg/redis/issues/298[298])
|
||||
Added support for authenticating users with an empty password
|
||||
but a non-default username.
|
||||
* (Issue https://github.com/boostorg/redis/issues/318[318])
|
||||
Fixed a number of race conditions in the `cancel()` function
|
||||
of `connection` and `basic_connection` that could cause
|
||||
cancellations to be ignored.
|
||||
* (Issue https://github.com/boostorg/redis/issues/290[290])
|
||||
Fixed a problem that could cause an error during `HELLO`
|
||||
to make subsequent `HELLO` attempts during reconnection to fail.
|
||||
* (Issue https://github.com/boostorg/redis/issues/297[297])
|
||||
Errors during `HELLO` are now correctly logged.
|
||||
* (Issue https://github.com/boostorg/redis/issues/287[287])
|
||||
Fixed a bug causing an exception to be thrown when parsing
|
||||
a response that contains an intermediate error into a `generic_response`.
|
||||
|
||||
|
||||
== Boost 1.89
|
||||
|
||||
* (Pull request https://github.com/boostorg/redis/pull/256[256],
|
||||
https://github.com/boostorg/redis/pull/266[266] and
|
||||
https://github.com/boostorg/redis/pull/273[273])
|
||||
The following members in `connection` and `basic_connection` are now deprecated
|
||||
and will be removed in subsequent releases:
|
||||
* `next_layer()` and `next_layer_type`: there is no reason to access the underlying stream object directly.
|
||||
Connection member functions should be used, instead.
|
||||
* `get_ssl_context()`: SSL contexts should never be modified after an `asio::ssl::stream`
|
||||
object has been created from them. Properties should be set before passing the context
|
||||
to the constructor. There is no reason to access the SSL context after that.
|
||||
* `reset_stream()`: connection internals have been refactored to reset the SSL stream
|
||||
automatically when required. This function is now a no-op.
|
||||
* The `async_run()` overload taking no parameters: use the `async_run`
|
||||
overload taking a `config` object explicitly, instead.
|
||||
* (Issue https://github.com/boostorg/redis/issues/213[213])
|
||||
The logging interface has been re-written:
|
||||
* Logging can now be customized by passing a function object
|
||||
to the `logger` constructor. This allows integration with
|
||||
third-party logging libraries, like spdlog.
|
||||
This new logging interface is public and will be maintained long-term.
|
||||
* The old, unstable interface consisting of `logger::on_xxx` functions has been removed.
|
||||
* `connection` and `basic_connection` constructors now accept a `logger` object.
|
||||
This is now the preferred way to configure logging.
|
||||
The `async_run()` overload taking a `logger` object is now deprecated, and will
|
||||
be removed in subsequent releases.
|
||||
* `config::log_prefix` is now deprecated, and will
|
||||
be removed in subsequent releases. Users can achieve the same effect
|
||||
by passing a custom logging function to the `logger` constructor.
|
||||
* The default logging function, which prints to `stderr`,
|
||||
is now based on `printf` and is thus thread-safe.
|
||||
The old function used `std::cerr` and could result to interleaved
|
||||
output in multi-threaded programs.
|
||||
* The default log level is now `logger::level::info`,
|
||||
down from `logger::level::debug`. This results in less verbose output by default.
|
||||
* (Issue https://github.com/boostorg/redis/issues/272[272])
|
||||
Added support for connecting to Redis using UNIX domain sockets.
|
||||
This feature can be accessed using `config::unix_socket`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/255[255])
|
||||
Fixed an issue that caused `async_run` to complete when a connection
|
||||
establishment error is encountered, even if `config::reconnect_wait_interval`
|
||||
specified reconnection.
|
||||
* (Issue https://github.com/boostorg/redis/issues/265[265])
|
||||
`connection::async_exec` now uses `ignore` as the default response,
|
||||
rather than having no default response. This matches
|
||||
`basic_connection::async_exec` behavior.
|
||||
* (Issue https://github.com/boostorg/redis/issues/260[260])
|
||||
Fixed a memory corruption affecting the logger's prefix
|
||||
configured in `config::log_prefix`.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/254[254])
|
||||
Fixed some warnings regarding unused variables, name shadowing
|
||||
and narrowing conversions.
|
||||
* (Issue https://github.com/boostorg/redis/issues/252[252])
|
||||
Fixed a bug that causes reconnection to ignore the reconnection
|
||||
wait time configured in `config::reconnect_wait_interval`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/238[238])
|
||||
Fixed a bug introduced in Boost 1.88 that caused `response<T>` to
|
||||
not compile for some integral types.
|
||||
* (Issue https://github.com/boostorg/redis/issues/247[247])
|
||||
The documentation has been modernized, and a more complete
|
||||
reference section has been added.
|
||||
* Part of the internals have been refactored to allow for better
|
||||
testing and reduce compile times.
|
||||
|
||||
|
||||
== Boost 1.88
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/233[233])
|
||||
|
||||
@@ -15,7 +15,7 @@ The examples below show how to use the features discussed throughout this docume
|
||||
* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions.
|
||||
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json.
|
||||
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf.
|
||||
* {site-url}/example/cpp20_resolve_with_sentinel.cpp[cpp20_resolve_with_sentinel.cpp]: Shows how to resolve a master address using sentinels.
|
||||
* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment.
|
||||
* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription.
|
||||
* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server.
|
||||
* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub.
|
||||
|
||||
@@ -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].
|
||||
@@ -95,40 +97,48 @@ them are:
|
||||
* https://redis.io/docs/manual/client-side-caching/[Client-side caching].
|
||||
|
||||
The connection class supports server pushes by means of the
|
||||
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive`] function, which can be
|
||||
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive2`] function, which can be
|
||||
called in the same connection that is being used to execute commands.
|
||||
The coroutine below shows how to use it:
|
||||
The coroutine below shows how to use it
|
||||
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Reconnect to channels.
|
||||
co_await conn->async_exec(req, ignore);
|
||||
|
||||
// Loop reading Redis pushes.
|
||||
for (;;) {
|
||||
error_code ec;
|
||||
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
// Use the response resp in some way and then clear it.
|
||||
...
|
||||
|
||||
consume_one(resp);
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -137,6 +147,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].
|
||||
|
||||
@@ -25,8 +25,12 @@ xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
|
||||
|
||||
xref:reference:boost/redis/address.adoc[`address`]
|
||||
|
||||
xref:reference:boost/redis/role.adoc[`role`]
|
||||
|
||||
xref:reference:boost/redis/config.adoc[`config`]
|
||||
|
||||
xref:reference:boost/redis/sentinel_config.adoc[`sentinel_config`]
|
||||
|
||||
xref:reference:boost/redis/error.adoc[`error`]
|
||||
|
||||
xref:reference:boost/redis/logger.adoc[`logger`]
|
||||
@@ -51,6 +55,8 @@ xref:reference:boost/redis/response.adoc[`response`]
|
||||
|
||||
xref:reference:boost/redis/generic_response.adoc[`generic_response`]
|
||||
|
||||
xref:reference:boost/redis/generic_flat_response.adoc[`generic_flat_response`]
|
||||
|
||||
xref:reference:boost/redis/consume_one-08.adoc[`consume_one`]
|
||||
|
||||
|
||||
@@ -66,25 +72,33 @@ xref:reference:boost/redis/adapter/result.adoc[`adapter::result`]
|
||||
xref:reference:boost/redis/any_adapter.adoc[`any_adapter`]
|
||||
|
||||
|
|
||||
xref:reference:boost/redis/resp3/basic_node.adoc[`basic_node`]
|
||||
xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`]
|
||||
|
||||
xref:reference:boost/redis/resp3/node.adoc[`node`]
|
||||
xref:reference:boost/redis/resp3/node.adoc[`resp3::node`]
|
||||
|
||||
xref:reference:boost/redis/resp3/node_view.adoc[`node_view`]
|
||||
xref:reference:boost/redis/resp3/node_view.adoc[`resp3::node_view`]
|
||||
|
||||
xref:reference:boost/redis/resp3/basic_tree.adoc[`resp3::basic_tree`]
|
||||
|
||||
xref:reference:boost/redis/resp3/tree.adoc[`resp3::tree`]
|
||||
|
||||
xref:reference:boost/redis/resp3/view_tree.adoc[`resp3::view_tree`]
|
||||
|
||||
xref:reference:boost/redis/resp3/flat_tree.adoc[`resp3::flat_tree`]
|
||||
|
||||
xref:reference:boost/redis/resp3/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`]
|
||||
|
||||
xref:reference:boost/redis/resp3/type.adoc[`type`]
|
||||
xref:reference:boost/redis/resp3/type.adoc[`resp3::type`]
|
||||
|
||||
xref:reference:boost/redis/resp3/is_aggregate.adoc[`is_aggregate`]
|
||||
xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`]
|
||||
|
||||
|
||||
|
|
||||
|
||||
xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parser.adoc[`parser`]
|
||||
xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parse.adoc[`parse`]
|
||||
xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`]
|
||||
|
||||
|===
|
||||
@@ -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.
|
||||
|
||||
@@ -184,7 +184,7 @@ must **NOT** be included in the response tuple. For example, the following reque
|
||||
----
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.subscribe({"channel"});
|
||||
req.push("QUIT");
|
||||
----
|
||||
|
||||
@@ -278,7 +278,8 @@ struct basic_node {
|
||||
----
|
||||
|
||||
Any response to a Redis command can be parsed into a
|
||||
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response].
|
||||
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response]
|
||||
and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost::redis::generic_flat_response].
|
||||
The vector can be seen as a pre-order view of the response tree.
|
||||
Using it is not different than using other types:
|
||||
|
||||
@@ -292,7 +293,7 @@ co_await conn->async_exec(req, resp);
|
||||
For example, suppose we want to retrieve a hash data structure
|
||||
from Redis with `HGETALL`, some of the options are
|
||||
|
||||
* `boost::redis::generic_response`: always works.
|
||||
* `boost::redis::generic_response` and `boost::redis::generic_flat_response`: always works.
|
||||
* `std::vector<std::string>`: efficient and flat, all elements as string.
|
||||
* `std::map<std::string, std::string>`: efficient if you need the data as a `std::map`.
|
||||
* `std::map<U, V>`: efficient if you are storing serialized data. Avoids temporaries and requires `boost_redis_from_bulk` for `U` and `V`.
|
||||
|
||||
152
doc/modules/ROOT/pages/sentinel.adoc
Normal file
152
doc/modules/ROOT/pages/sentinel.adoc
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
|
||||
= Sentinel
|
||||
|
||||
Boost.Redis supports Redis Sentinel deployments. Sentinel handling
|
||||
in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`]
|
||||
automatically connects to Sentinels, resolves the master's address, and connects to the master.
|
||||
|
||||
Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
config cfg;
|
||||
|
||||
// To enable Sentinel, set this field to a non-empty list
|
||||
// of (hostname, port) pairs where Sentinels are listening
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
|
||||
// Set master_name to the identifier that you configured
|
||||
// in the "sentinel monitor" statement of your sentinel.conf file
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
----
|
||||
|
||||
Once set, the connection object can be used normally. See our
|
||||
our {site-url}/example/cpp20_sentinel.cpp[Sentinel example]
|
||||
for a full program.
|
||||
|
||||
== Connecting to replicas
|
||||
|
||||
By default, the library connects to the Redis master.
|
||||
You can connect to one of its replicas by using
|
||||
xref:reference:boost/redis/sentinel_config/server_role.adoc[`config::sentinel::server_role`].
|
||||
This can be used to balance load, if all your commands read data from
|
||||
the server and never write to it. The particular replica will be chosen randomly.
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
config cfg;
|
||||
|
||||
// Set up Sentinel
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Ask the library to connect to a random replica of 'mymaster', rather than the master node
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
----
|
||||
|
||||
|
||||
== Sentinel authentication
|
||||
|
||||
If your Sentinels require authentication,
|
||||
you can use xref:reference:boost/redis/sentinel_config/setup.adoc[`config::sentinel::setup`]
|
||||
to provide credentials.
|
||||
This request is executed immediately after connecting to Sentinels, and
|
||||
before any other command:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Set up Sentinel
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// By default, setup contains a 'HELLO 3' command.
|
||||
// Override it to add an AUTH clause to it with out credentials.
|
||||
cfg.sentinel.setup.clear();
|
||||
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_password");
|
||||
|
||||
// cfg.sentinel.setup applies to Sentinels, only.
|
||||
// Use cfg.setup to authenticate to masters/replicas.
|
||||
cfg.use_setup = true; // Required for cfg.setup to be used, for historic reasons
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("HELLO", 3, "AUTH", "master_user", "master_password");
|
||||
----
|
||||
|
||||
== Using TLS with Sentinels
|
||||
|
||||
You might use TLS with Sentinels only, masters/replicas only, or both by adjusting
|
||||
xref:reference:boost/redis/sentinel_config/use_ssl.adoc[`config::sentinel::use_ssl`]
|
||||
and xref:reference:boost/redis/config/use_ssl.adoc[`config::use_ssl`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Set up Sentinel
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Adjust these switches to enable/disable TLS
|
||||
cfg.use_ssl = true; // Applies to masters and replicas
|
||||
cfg.sentinel.use_ssl = true; // Applies to Sentinels
|
||||
----
|
||||
|
||||
== Sentinel algorithm
|
||||
|
||||
This section details how `async_run` interacts with Sentinel.
|
||||
Most of the algorithm follows
|
||||
https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines].
|
||||
Some of these details may vary between library versions.
|
||||
|
||||
* Connections maintain an internal list of Sentinels, bootstrapped from
|
||||
xref:reference:boost/redis/sentinel_config/addresses.adoc[`config::sentinel::addresses`].
|
||||
* The first Sentinel in the list is contacted by performing the following:
|
||||
** A physical connection is established.
|
||||
** The setup request is executed.
|
||||
** The master's address is resolved using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL GET-MASTER-NAME-BY-ADDR`].
|
||||
** If `config::sentinel::server_role` is `role::replica`, replica addresses are obtained using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL REPLICAS`].
|
||||
One replica is chosen randomly.
|
||||
** The address of other Sentinels also monitoring this master are retrieved using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL SENTINELS`].
|
||||
* If a Sentinel is unreachable, doesn't know about the configured master,
|
||||
or returns an error while executing the above requests, the next Sentinel in the list is tried.
|
||||
* If all Sentinels have been tried without success, `config::reconnect_wait_interval`
|
||||
is waited, and the process starts again.
|
||||
* After a successful Sentinel response, the internal Sentinel list is updated
|
||||
with any newly discovered Sentinels.
|
||||
Sentinels in `config::sentinel::addresses` are always kept in the list,
|
||||
even if they weren't present in the output of `SENTINEL SENTINELS`.
|
||||
* The retrieved address is used
|
||||
to establish a connection with the master or replica.
|
||||
A `ROLE` command is added at the end of the setup request.
|
||||
This is used to detect situations where a Sentinel returns outdated
|
||||
information due to a failover in process. If `ROLE` doesn't output
|
||||
the expected role (`"master"` or `"slave"`, depending on `config::sentinel::server_role`)
|
||||
`config::reconnect_wait_interval` is waited and Sentinel is contacted again.
|
||||
* The connection to the master/replica is run like any other connection.
|
||||
If network errors or timeouts happen, `config::reconnect_wait_interval`
|
||||
is waited and Sentinel is contacted again.
|
||||
@@ -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_testable_example(cpp20_sentinel 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
|
||||
|
||||
@@ -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)
|
||||
@@ -6,12 +6,15 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/as_tuple.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/redirect_error.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -27,12 +30,9 @@ using boost::asio::co_spawn;
|
||||
using boost::asio::consign;
|
||||
using boost::asio::detached;
|
||||
using boost::asio::dynamic_buffer;
|
||||
using boost::asio::redirect_error;
|
||||
using boost::asio::use_awaitable;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::generic_flat_response;
|
||||
using boost::redis::request;
|
||||
using boost::system::error_code;
|
||||
using namespace std::chrono_literals;
|
||||
@@ -40,27 +40,44 @@ using namespace std::chrono_literals;
|
||||
// Chat over Redis pubsub. To test, run this program from multiple
|
||||
// terminals and type messages to stdin.
|
||||
|
||||
namespace {
|
||||
|
||||
auto rethrow_on_error = [](std::exception_ptr exc) {
|
||||
if (exc)
|
||||
std::rethrow_exception(exc);
|
||||
};
|
||||
|
||||
auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
// Set the receive response, so pushes are stored in resp
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
while (conn->will_reconnect()) {
|
||||
// Subscribe to channels.
|
||||
co_await conn->async_exec(req, ignore);
|
||||
// Subscribe to the channel 'channel'. Using request::subscribe()
|
||||
// (instead of request::push()) makes the connection re-subscribe
|
||||
// to 'channel' whenever it re-connects to the server.
|
||||
request req;
|
||||
req.subscribe({"channel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// Loop reading Redis push messages.
|
||||
for (error_code ec;;) {
|
||||
co_await conn->async_receive(redirect_error(use_awaitable, ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
|
||||
<< resp.value().at(3).value << std::endl;
|
||||
resp.value().clear();
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +89,13 @@ auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection
|
||||
auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n");
|
||||
request req;
|
||||
req.push("PUBLISH", "channel", msg);
|
||||
co_await conn->async_exec(req, ignore);
|
||||
co_await conn->async_exec(req);
|
||||
msg.erase(0, n);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> awaitable<void>
|
||||
{
|
||||
@@ -84,8 +103,8 @@ auto co_main(config cfg) -> awaitable<void>
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
|
||||
|
||||
co_spawn(ex, receiver(conn), detached);
|
||||
co_spawn(ex, publisher(stream, conn), detached);
|
||||
co_spawn(ex, receiver(conn), rethrow_on_error);
|
||||
co_spawn(ex, publisher(stream, conn), rethrow_on_error);
|
||||
conn->async_run(cfg, consign(detached, conn));
|
||||
|
||||
signal_set sig_set{ex, SIGINT, SIGTERM};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,76 +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 <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using endpoints = asio::ip::tcp::resolver::results_type;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::config;
|
||||
using boost::redis::address;
|
||||
using boost::redis::connection;
|
||||
|
||||
auto redir(boost::system::error_code& ec) { return asio::redirect_error(asio::use_awaitable, ec); }
|
||||
|
||||
// For more info see
|
||||
// - https://redis.io/docs/manual/sentinel.
|
||||
// - https://redis.io/docs/reference/sentinel-clients.
|
||||
auto resolve_master_address(std::vector<address> const& addresses) -> asio::awaitable<address>
|
||||
{
|
||||
request req;
|
||||
req.push("SENTINEL", "get-master-addr-by-name", "mymaster");
|
||||
req.push("QUIT");
|
||||
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
|
||||
response<std::optional<std::array<std::string, 2>>, ignore_t> resp;
|
||||
for (auto addr : addresses) {
|
||||
boost::system::error_code ec;
|
||||
config cfg;
|
||||
cfg.addr = addr;
|
||||
// TODO: async_run and async_exec should be lauched in
|
||||
// parallel here so we can wait for async_run completion
|
||||
// before eventually calling it again.
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
co_await conn->async_exec(req, resp, redir(ec));
|
||||
conn->cancel();
|
||||
if (!ec && std::get<0>(resp))
|
||||
co_return address{
|
||||
std::get<0>(resp).value().value().at(0),
|
||||
std::get<0>(resp).value().value().at(1)};
|
||||
}
|
||||
|
||||
co_return address{};
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
// A list of sentinel addresses from which only one is responsive.
|
||||
// This simulates sentinels that are down.
|
||||
std::vector<address> const addresses{
|
||||
address{"foo", "26379"},
|
||||
address{"bar", "26379"},
|
||||
cfg.addr
|
||||
};
|
||||
|
||||
auto const ep = co_await resolve_master_address(addresses);
|
||||
|
||||
std::clog << "Host: " << ep.host << "\n"
|
||||
<< "Port: " << ep.port << "\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
60
example/cpp20_sentinel.cpp
Normal file
60
example/cpp20_sentinel.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// 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/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;
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
// Boost.Redis has built-in support for Sentinel deployments.
|
||||
// To enable it, set the fields in config shown here.
|
||||
// sentinel.addresses should contain a list of (hostname, port) pairs
|
||||
// where Sentinels are listening. IPs can also be used.
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
|
||||
// Set master_name to the identifier that you configured
|
||||
// in the "sentinel monitor" statement of your sentinel.conf file
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// async_run will contact the Sentinels, obtain the master address,
|
||||
// connect to it and keep the connection healthy. If a failover happens,
|
||||
// the address will be resolved again and the new elected master will be contacted.
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
// You can now use the connection normally, as you would use a connection to a single master.
|
||||
request req;
|
||||
req.push("PING", "Hello world");
|
||||
response<std::string> resp;
|
||||
|
||||
// Execute the request.
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -1,19 +1,17 @@
|
||||
/* 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)
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
#include <boost/asio/as_tuple.hpp>
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@@ -22,12 +20,8 @@
|
||||
namespace asio = boost::asio;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::redis::request;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::consume_one;
|
||||
using boost::redis::logger;
|
||||
using boost::redis::generic_flat_response;
|
||||
using boost::redis::config;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::error;
|
||||
using boost::system::error_code;
|
||||
using boost::redis::connection;
|
||||
using asio::signal_set;
|
||||
@@ -37,7 +31,7 @@ using asio::signal_set;
|
||||
* To test send messages with redis-cli
|
||||
*
|
||||
* $ redis-cli -3
|
||||
* 127.0.0.1:6379> PUBLISH channel some-message
|
||||
* 127.0.0.1:6379> PUBLISH mychannel some-message
|
||||
* (integer) 3
|
||||
* 127.0.0.1:6379>
|
||||
*
|
||||
@@ -51,34 +45,39 @@ using asio::signal_set;
|
||||
// Receives server pushes.
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Reconnect to the channels.
|
||||
co_await conn->async_exec(req, ignore);
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Loop reading Redis pushs messages.
|
||||
for (error_code ec;;) {
|
||||
// First tries to read any buffered pushes.
|
||||
conn->receive(ec);
|
||||
if (ec == error::sync_receive_push_failed) {
|
||||
ec = {};
|
||||
co_await conn->async_receive(asio::redirect_error(asio::use_awaitable, ec));
|
||||
}
|
||||
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
|
||||
<< resp.value().at(3).value << std::endl;
|
||||
|
||||
consume_one(resp);
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
@@ -12,9 +12,7 @@
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace boost::redis {
|
||||
@@ -50,20 +48,7 @@ public:
|
||||
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;
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
static auto create_impl(T& resp) -> impl_t;
|
||||
|
||||
/// Contructs from a type erased adaper
|
||||
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
|
||||
@@ -109,6 +94,32 @@ private:
|
||||
impl_t impl_;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <class Adapter>
|
||||
any_adapter::impl_t make_any_adapter_impl(Adapter&& value)
|
||||
{
|
||||
return [adapter = std::move(value)](
|
||||
any_adapter::parse_event ev,
|
||||
resp3::node_view const& nd,
|
||||
system::error_code& ec) mutable {
|
||||
switch (ev) {
|
||||
case any_adapter::parse_event::init: adapter.on_init(); break;
|
||||
case any_adapter::parse_event::node: adapter.on_node(nd, ec); break;
|
||||
case any_adapter::parse_event::done: adapter.on_done(); break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
template <class T>
|
||||
auto boost::redis::any_adapter::create_impl(T& resp) -> impl_t
|
||||
{
|
||||
using adapter::boost_redis_adapt;
|
||||
return detail::make_any_adapter_impl(boost_redis_adapt(resp));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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)
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
@@ -164,16 +166,111 @@ 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)}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
class general_aggregate<resp3::tree> {
|
||||
private:
|
||||
resp3::tree* tree_ = nullptr;
|
||||
|
||||
public:
|
||||
explicit general_aggregate(resp3::tree* c = nullptr)
|
||||
: tree_(c)
|
||||
{ }
|
||||
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
|
||||
|
||||
resp3::node tmp;
|
||||
tmp.data_type = nd.data_type;
|
||||
tmp.aggregate_size = nd.aggregate_size;
|
||||
tmp.depth = nd.depth;
|
||||
tmp.value = std::string{std::cbegin(nd.value), std::cend(nd.value)};
|
||||
|
||||
tree_->push_back(std::move(tmp));
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
class general_aggregate<generic_flat_response> {
|
||||
private:
|
||||
generic_flat_response* tree_ = nullptr;
|
||||
|
||||
public:
|
||||
explicit general_aggregate(generic_flat_response* c = nullptr)
|
||||
: tree_(c)
|
||||
{ }
|
||||
|
||||
void on_init()
|
||||
{
|
||||
if (tree_->has_value()) {
|
||||
tree_->value().notify_init();
|
||||
}
|
||||
}
|
||||
void on_done()
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
|
||||
if (tree_->has_value()) {
|
||||
tree_->value().notify_done();
|
||||
}
|
||||
}
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::blob_error:
|
||||
case resp3::type::simple_error:
|
||||
*tree_ = error{
|
||||
nd.data_type,
|
||||
std::string{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
if (tree_->has_value()) {
|
||||
(**tree_).push(nd);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
class general_aggregate<resp3::flat_tree> {
|
||||
private:
|
||||
resp3::flat_tree* tree_ = nullptr;
|
||||
|
||||
public:
|
||||
explicit general_aggregate(resp3::flat_tree* c = nullptr)
|
||||
: tree_(c)
|
||||
{ }
|
||||
|
||||
void on_init() { tree_->notify_init(); }
|
||||
void on_done() { tree_->notify_done(); }
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
|
||||
tree_->push(nd);
|
||||
}
|
||||
};
|
||||
|
||||
template <class Node>
|
||||
class general_simple {
|
||||
private:
|
||||
|
||||
@@ -92,8 +92,32 @@ struct response_traits<result<ignore_t>> {
|
||||
};
|
||||
|
||||
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>>;
|
||||
struct response_traits<result<resp3::basic_tree<String, Allocator>>> {
|
||||
using response_type = result<resp3::basic_tree<String, Allocator>>;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <class String>
|
||||
struct response_traits<resp3::basic_tree<String>> {
|
||||
using response_type = resp3::basic_tree<String>;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct response_traits<resp3::flat_tree> {
|
||||
using response_type = resp3::flat_tree;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct response_traits<generic_flat_response> {
|
||||
using response_type = generic_flat_response;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
|
||||
#include <boost/mp11.hpp>
|
||||
|
||||
@@ -56,12 +58,33 @@ struct result_traits<result<resp3::basic_node<T>>> {
|
||||
};
|
||||
|
||||
template <class String, class Allocator>
|
||||
struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
|
||||
struct result_traits<result<resp3::basic_tree<String, Allocator>>> {
|
||||
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
|
||||
using adapter_type = adapter::detail::general_aggregate<response_type>;
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <class String>
|
||||
struct result_traits<resp3::basic_tree<String>> {
|
||||
using response_type = resp3::basic_tree<String>;
|
||||
using adapter_type = adapter::detail::general_aggregate<response_type>;
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct result_traits<generic_flat_response> {
|
||||
using response_type = generic_flat_response;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct result_traits<resp3::flat_tree> {
|
||||
using response_type = resp3::flat_tree;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <class T>
|
||||
using adapter_t = typename result_traits<std::decay_t<T>>::adapter_type;
|
||||
|
||||
|
||||
@@ -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,10 +7,13 @@
|
||||
#ifndef BOOST_REDIS_CONFIG_HPP
|
||||
#define BOOST_REDIS_CONFIG_HPP
|
||||
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
@@ -22,12 +25,130 @@ struct address {
|
||||
std::string port = "6379";
|
||||
};
|
||||
|
||||
/// Configure parameters used by the connection classes.
|
||||
struct config {
|
||||
/// Uses SSL instead of a plain connection.
|
||||
/** @brief Compares two addresses for equality.
|
||||
* @relates address
|
||||
*
|
||||
* @param a Left hand side address.
|
||||
* @param b Right hand side address.
|
||||
*/
|
||||
inline bool operator==(address const& a, address const& b)
|
||||
{
|
||||
return a.host == b.host && a.port == b.port;
|
||||
}
|
||||
|
||||
/** @brief Compares two addresses for inequality.
|
||||
* @relates address
|
||||
*
|
||||
* @param a Left hand side address.
|
||||
* @param b Right hand side address.
|
||||
*/
|
||||
inline bool operator!=(address const& a, address const& b) { return !(a == b); }
|
||||
|
||||
/// Identifies the possible roles of a Redis server.
|
||||
enum class role
|
||||
{
|
||||
/// The server is a master.
|
||||
master,
|
||||
|
||||
/// The server is a replica.
|
||||
replica,
|
||||
};
|
||||
|
||||
/// Configuration values to use when using Sentinel.
|
||||
struct sentinel_config {
|
||||
/**
|
||||
* @brief A list of (hostname, port) pairs where the Sentinels are listening.
|
||||
*
|
||||
* Sentinels in this list will be contacted in order, until a successful
|
||||
* connection is made. At this point, the `SENTINEL SENTINELS` command
|
||||
* will be used to retrieve any additional Sentinels monitoring the configured master.
|
||||
* Thus, it is not required to keep this list comprehensive - if Sentinels are added
|
||||
* later, they will be detected at runtime.
|
||||
*
|
||||
* Sentinel will only be used if this value is not empty.
|
||||
*
|
||||
* Numeric IP addresses are also allowed as hostnames.
|
||||
*/
|
||||
std::vector<address> addresses{};
|
||||
|
||||
/**
|
||||
* @brief The name of the master to connect to, as configured in the
|
||||
* `sentinel monitor` statement in `sentinel.conf`.
|
||||
*
|
||||
* This field is required even when connecting to replicas.
|
||||
*/
|
||||
std::string master_name{};
|
||||
|
||||
/**
|
||||
* @brief Whether connections to Sentinels should use TLS or not.
|
||||
* Does not affect connections to masters.
|
||||
*
|
||||
* When set to `true`, physical connections to Sentinels will be established
|
||||
* using TLS. This setting does *not* influence how masters and replicas are contacted.
|
||||
* To use TLS when connecting to these, set @ref config::use_ssl to `true`.
|
||||
*/
|
||||
bool use_ssl = false;
|
||||
|
||||
/// For TCP connections, hostname and port of the Redis server.
|
||||
/**
|
||||
* @brief A request to be sent to Sentinels upon connection establishment.
|
||||
*
|
||||
* This request is executed every time a Sentinel is contacted, and before
|
||||
* commands like `SENTINEL GET-MASTER-NAME-BY-ADDR` are run.
|
||||
* By default, this field contains a `HELLO 3` command.
|
||||
* You can use this request to set up any authorization required by Sentinels.
|
||||
*
|
||||
* This request should ensure that the connection is upgraded to RESP3
|
||||
* by executing `HELLO 3` or similar. RESP2 is not supported yet.
|
||||
*/
|
||||
request setup = detail::make_hello_request();
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel resolve operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::resolve_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration resolve_timeout = std::chrono::milliseconds{500};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel connect operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::connect_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration connect_timeout = std::chrono::milliseconds{500};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel TLS handshake operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::ssl_handshake_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{5};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel request/response exchange is allowed to elapse.
|
||||
* Includes executing the commands in @ref setup and the commands required to
|
||||
* resolve the server's address.
|
||||
*/
|
||||
std::chrono::steady_clock::duration request_timeout = std::chrono::seconds{5};
|
||||
|
||||
/**
|
||||
* @brief Whether to connect to a Redis master or to a replica.
|
||||
*
|
||||
* The library resolves and connects to the Redis master, by default.
|
||||
* Set this value to @ref role::replica to connect to one of the replicas
|
||||
* of the master identified by @ref master_name.
|
||||
* The particular replica will be chosen randomly.
|
||||
*/
|
||||
role server_role = role::master;
|
||||
};
|
||||
|
||||
/// Configure parameters used by the connection classes.
|
||||
struct config {
|
||||
/**
|
||||
* @brief Whether to use TLS instead of plaintext connections.
|
||||
*
|
||||
* When using Sentinel, configures whether to use TLS when connecting to masters and replicas.
|
||||
* Use @ref sentinel_config::use_ssl to control TLS for Sentinels.
|
||||
*/
|
||||
bool use_ssl = false;
|
||||
|
||||
/// For TCP connections, hostname and port of the Redis server. Ignored when using Sentinel.
|
||||
address addr = address{"127.0.0.1", "6379"};
|
||||
|
||||
/**
|
||||
@@ -35,29 +156,67 @@ struct config {
|
||||
*
|
||||
* If non-empty, communication with the server will happen using
|
||||
* UNIX domain sockets, and @ref addr will be ignored.
|
||||
*
|
||||
* UNIX domain sockets can't be used with SSL: if `unix_socket` is non-empty,
|
||||
* @ref use_ssl must be `false`.
|
||||
* @ref use_ssl must be `false`. UNIX domain sockets can't be used with Sentinel, either.
|
||||
*
|
||||
* UNIX domain sockets can't be used with Sentinel.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure this value for Sentinels.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -69,40 +228,123 @@ struct config {
|
||||
*/
|
||||
std::string log_prefix = "(Boost.Redis) ";
|
||||
|
||||
/// Time span that the resolve operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the resolve operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time span that the connect operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the connect operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time span that the SSL handshake operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the SSL handshake operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Sentinels are not health-checked.
|
||||
*/
|
||||
std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2};
|
||||
|
||||
/** @brief Time span to wait between successive connection retries.
|
||||
* Set to zero to disable reconnection.
|
||||
* Set to zero to disable reconnection.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
* If none of the configured Sentinels can be contacted, this time span will
|
||||
* be waited before trying again. After a connection error with a master or replica
|
||||
* is encountered, this time span will be waited before contacting Sentinels again.
|
||||
*/
|
||||
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
|
||||
|
||||
/** @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.
|
||||
* Sets a limit on how much data is allowed to be read into the
|
||||
* read buffer. It can be used to prevent DDOS.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
*/
|
||||
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
|
||||
|
||||
/** @brief read_buffer_append_size
|
||||
/** @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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup for Sentinels.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup for Sentinels.
|
||||
*/
|
||||
request setup = detail::make_hello_request();
|
||||
|
||||
/**
|
||||
* @brief Configuration values for Sentinel. Sentinel is enabled only if
|
||||
* @ref sentinel_config::addresses is not empty.
|
||||
*/
|
||||
sentinel_config sentinel{};
|
||||
};
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
91
include/boost/redis/detail/connect_fsm.hpp
Normal file
91
include/boost/redis/detail/connect_fsm.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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/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};
|
||||
buffered_logger* lgr_{nullptr};
|
||||
|
||||
public:
|
||||
connect_fsm(buffered_logger& lgr) noexcept
|
||||
: lgr_(&lgr)
|
||||
{ }
|
||||
|
||||
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
|
||||
65
include/boost/redis/detail/connect_params.hpp
Normal file
65
include/boost/redis/detail/connect_params.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// 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_PARAMS_HPP
|
||||
#define BOOST_REDIS_CONNECT_PARAMS_HPP
|
||||
|
||||
// Parameters used by redis_stream::async_connect
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_fsm.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Fully identifies where a server is listening. Reference type.
|
||||
class any_address_view {
|
||||
transport_type type_;
|
||||
union {
|
||||
const address* tcp_;
|
||||
std::string_view unix_;
|
||||
};
|
||||
|
||||
public:
|
||||
any_address_view(const address& addr, bool use_ssl) noexcept
|
||||
: type_(use_ssl ? transport_type::tcp_tls : transport_type::tcp)
|
||||
, tcp_(&addr)
|
||||
{ }
|
||||
|
||||
explicit any_address_view(std::string_view unix_socket) noexcept
|
||||
: type_(transport_type::unix_socket)
|
||||
, unix_(unix_socket)
|
||||
{ }
|
||||
|
||||
transport_type type() const { return type_; }
|
||||
|
||||
const address& tcp_address() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == transport_type::tcp || type_ == transport_type::tcp_tls);
|
||||
return *tcp_;
|
||||
}
|
||||
|
||||
std::string_view unix_socket() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == transport_type::unix_socket);
|
||||
return unix_;
|
||||
}
|
||||
};
|
||||
|
||||
struct connect_params {
|
||||
any_address_view addr;
|
||||
std::chrono::steady_clock::duration resolve_timeout;
|
||||
std::chrono::steady_clock::duration connect_timeout;
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout;
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -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
|
||||
64
include/boost/redis/detail/connection_state.hpp
Normal file
64
include/boost/redis/detail/connection_state.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// 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/detail/subscription_tracker.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// A random engine that gets seeded lazily.
|
||||
// Seeding with std::random_device is not trivial and might fail.
|
||||
class lazy_random_engine {
|
||||
bool seeded_{};
|
||||
std::minstd_rand eng_;
|
||||
|
||||
public:
|
||||
lazy_random_engine() = default;
|
||||
std::minstd_rand& get()
|
||||
{
|
||||
if (!seeded_) {
|
||||
eng_.seed(static_cast<std::minstd_rand::result_type>(std::random_device{}()));
|
||||
seeded_ = true;
|
||||
}
|
||||
return eng_;
|
||||
}
|
||||
};
|
||||
|
||||
// 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 diagnostic{}; // Used by the setup request and Sentinel
|
||||
request setup_req{};
|
||||
request ping_req{};
|
||||
subscription_tracker tracker{};
|
||||
bool receive2_running{false}, receive2_cancelled{false};
|
||||
|
||||
// Sentinel stuff
|
||||
lazy_random_engine eng{};
|
||||
std::vector<address> sentinels{};
|
||||
std::vector<resp3::node> sentinel_resp_nodes{}; // for parsing
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
struct connection_state;
|
||||
|
||||
// What should we do next?
|
||||
enum class exec_action_type
|
||||
{
|
||||
@@ -29,7 +31,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 {
|
||||
@@ -55,16 +56,17 @@ public:
|
||||
|
||||
class exec_fsm {
|
||||
int resume_point_{0};
|
||||
multiplexer* mpx_{nullptr};
|
||||
std::shared_ptr<multiplexer::elem> elem_;
|
||||
|
||||
public:
|
||||
exec_fsm(multiplexer& mpx, std::shared_ptr<multiplexer::elem> elem) noexcept
|
||||
: mpx_(&mpx)
|
||||
, elem_(std::move(elem))
|
||||
exec_fsm(std::shared_ptr<multiplexer::elem> elem) noexcept
|
||||
: elem_(std::move(elem))
|
||||
{ }
|
||||
|
||||
exec_action resume(bool connection_is_open, asio::cancellation_type_t cancel_state);
|
||||
exec_action resume(
|
||||
bool connection_is_open,
|
||||
connection_state& st,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
69
include/boost/redis/detail/exec_one_fsm.hpp
Normal file
69
include/boost/redis/detail/exec_one_fsm.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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_EXEC_ONE_FSM_HPP
|
||||
#define BOOST_REDIS_EXEC_ONE_FSM_HPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// Sans-io algorithm for async_exec_one, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
class read_buffer;
|
||||
|
||||
// What should we do next?
|
||||
enum class exec_one_action_type
|
||||
{
|
||||
done, // Call the final handler
|
||||
write, // Write the request
|
||||
read_some, // Read into the read buffer
|
||||
};
|
||||
|
||||
struct exec_one_action {
|
||||
exec_one_action_type type;
|
||||
system::error_code ec;
|
||||
|
||||
exec_one_action(exec_one_action_type type) noexcept
|
||||
: type{type}
|
||||
{ }
|
||||
|
||||
exec_one_action(system::error_code ec) noexcept
|
||||
: type{exec_one_action_type::done}
|
||||
, ec{ec}
|
||||
{ }
|
||||
};
|
||||
|
||||
class exec_one_fsm {
|
||||
int resume_point_{0};
|
||||
any_adapter adapter_;
|
||||
std::size_t remaining_responses_;
|
||||
resp3::parser parser_;
|
||||
|
||||
public:
|
||||
exec_one_fsm(any_adapter resp, std::size_t expected_responses)
|
||||
: adapter_(std::move(resp))
|
||||
, remaining_responses_(expected_responses)
|
||||
{ }
|
||||
|
||||
exec_one_action resume(
|
||||
read_buffer& buffer,
|
||||
system::error_code ec,
|
||||
std::size_t bytes_transferred,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -1,196 +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 ConnectionImpl>
|
||||
class ping_op {
|
||||
public:
|
||||
HealthChecker* checker_ = nullptr;
|
||||
ConnectionImpl* 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 ConnectionImpl, class CompletionToken>
|
||||
auto async_ping(ConnectionImpl& conn, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
ping_op<health_checker, ConnectionImpl>{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
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <boost/redis/adapter/adapt.hpp>
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
@@ -17,11 +18,10 @@
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@@ -31,7 +31,13 @@ class request;
|
||||
|
||||
namespace detail {
|
||||
|
||||
using tribool = std::optional<bool>;
|
||||
// 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:
|
||||
@@ -91,6 +97,16 @@ public:
|
||||
|
||||
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
|
||||
{
|
||||
@@ -112,22 +128,30 @@ public:
|
||||
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(std::string_view data, 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]]
|
||||
@@ -136,24 +160,44 @@ public:
|
||||
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
|
||||
auto get_read_buffer() noexcept -> read_buffer&
|
||||
{
|
||||
return std::string_view{write_buffer_};
|
||||
return read_buffer_;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_prepared_read_buffer() noexcept -> read_buffer::span_type;
|
||||
|
||||
[[nodiscard]]
|
||||
auto prepare_read() noexcept -> system::error_code;
|
||||
|
||||
void commit_read(std::size_t read_size);
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_read_buffer_size() const noexcept -> std::size_t;
|
||||
|
||||
void set_receive_adapter(any_adapter adapter);
|
||||
|
||||
[[nodiscard]]
|
||||
@@ -162,26 +206,23 @@ public:
|
||||
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, std::size_t size);
|
||||
void commit_usage(bool is_push, read_buffer::consume_result res);
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_next_push(std::string_view data) const noexcept -> bool;
|
||||
|
||||
// Releases the number of requests that have been released.
|
||||
[[nodiscard]]
|
||||
auto release_push_requests() -> std::size_t;
|
||||
// Completes requests that don't expect a response
|
||||
void release_push_requests();
|
||||
|
||||
[[nodiscard]]
|
||||
tribool consume_next_impl(std::string_view data, system::error_code& ec);
|
||||
consume_result consume_impl(system::error_code& ec);
|
||||
|
||||
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;
|
||||
|
||||
@@ -21,30 +21,34 @@ 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_append() -> system::error_code;
|
||||
auto prepare() -> system::error_code;
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_append_buffer() noexcept -> span_type;
|
||||
auto get_prepared() noexcept -> span_type;
|
||||
|
||||
void commit_append(std::size_t read_size);
|
||||
void commit(std::size_t read_size);
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_committed_buffer() const noexcept -> std::string_view;
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_committed_size() const noexcept -> std::size_t;
|
||||
auto get_commited() const noexcept -> std::string_view;
|
||||
|
||||
void clear();
|
||||
|
||||
// Consume committed data.
|
||||
auto consume_committed(std::size_t size) -> std::size_t;
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
#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 {
|
||||
@@ -19,36 +22,74 @@ 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_ = 0u;
|
||||
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(read_buffer& rbuf, 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};
|
||||
read_buffer* read_buffer_ = nullptr;
|
||||
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
|
||||
|
||||
58
include/boost/redis/detail/receive_fsm.hpp
Normal file
58
include/boost/redis/detail/receive_fsm.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright (c) 2018-2026 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_RECEIVE_FSM_HPP
|
||||
#define BOOST_REDIS_RECEIVE_FSM_HPP
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// Sans-io algorithm for async_receive2, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
struct connection_state;
|
||||
|
||||
struct receive_action {
|
||||
enum class action_type
|
||||
{
|
||||
setup_cancellation, // Set up the cancellation types supported by the composed operation
|
||||
wait, // Wait for a message to appear in the receive channel
|
||||
drain_channel, // Empty the receive channel
|
||||
immediate, // Call async_immediate
|
||||
done, // Complete
|
||||
};
|
||||
|
||||
action_type type;
|
||||
system::error_code ec;
|
||||
|
||||
receive_action(action_type type) noexcept
|
||||
: type{type}
|
||||
{ }
|
||||
|
||||
receive_action(system::error_code ec) noexcept
|
||||
: type{action_type::done}
|
||||
, ec{ec}
|
||||
{ }
|
||||
};
|
||||
|
||||
class receive_fsm {
|
||||
int resume_point_{0};
|
||||
|
||||
public:
|
||||
receive_fsm() = default;
|
||||
|
||||
receive_action resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
@@ -8,8 +8,10 @@
|
||||
#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/detail/connect_params.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
#include <boost/asio/basic_waitable_timer.hpp>
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <utility>
|
||||
@@ -31,14 +34,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 +43,107 @@ 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{};
|
||||
redis_stream& obj_;
|
||||
connect_fsm fsm_;
|
||||
connect_params params_;
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Prevent use-after-move errors
|
||||
auto& obj = this->obj_;
|
||||
auto params = this->params_;
|
||||
|
||||
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(
|
||||
params.addr.unix_socket(),
|
||||
asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self)));
|
||||
#else
|
||||
BOOST_ASSERT(false);
|
||||
#endif
|
||||
return;
|
||||
|
||||
case connect_action_type::tcp_resolve:
|
||||
obj.resolv_.async_resolve(
|
||||
params.addr.tcp_address().host,
|
||||
params.addr.tcp_address().port,
|
||||
asio::cancel_after(obj.timer_, params.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_, params.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) {
|
||||
auto& obj = this->obj_; // prevent use-after-move errors
|
||||
asio::async_connect(
|
||||
obj.stream_.next_layer(),
|
||||
std::move(endpoints),
|
||||
asio::cancel_after(obj.timer_, params_.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 +166,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 +176,11 @@ public:
|
||||
|
||||
// I/O
|
||||
template <class CompletionToken>
|
||||
auto async_connect(const config* cfg, connection_logger* l, CompletionToken&& token)
|
||||
auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token)
|
||||
{
|
||||
this->st_.type = params.addr.type();
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
connect_op{*this, cfg, l},
|
||||
connect_op{*this, connect_fsm{l}, params},
|
||||
token);
|
||||
}
|
||||
|
||||
@@ -220,7 +188,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 +213,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 +237,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,115 +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 ConnectionImpl>
|
||||
struct hello_op {
|
||||
Handshaker* handshaker_ = nullptr;
|
||||
ConnectionImpl* 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 ConnectionImpl, class CompletionToken>
|
||||
auto async_hello(ConnectionImpl& conn, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
hello_op<resp3_handshaker, ConnectionImpl>{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
|
||||
67
include/boost/redis/detail/run_fsm.hpp
Normal file
67
include/boost/redis/detail/run_fsm.hpp
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// 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/redis/detail/connect_params.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
|
||||
sentinel_resolve, // Contact Sentinels to resolve the master's address
|
||||
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);
|
||||
};
|
||||
|
||||
connect_params make_run_connect_params(const connection_state& st);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
93
include/boost/redis/detail/sentinel_resolve_fsm.hpp
Normal file
93
include/boost/redis/detail/sentinel_resolve_fsm.hpp
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// 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_SENTINEL_RESOLVE_FSM_HPP
|
||||
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// Sans-io algorithm for async_sentinel_resolve, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Forward decls
|
||||
struct connection_state;
|
||||
|
||||
class sentinel_action {
|
||||
public:
|
||||
enum class type
|
||||
{
|
||||
done, // Call the final handler
|
||||
connect, // Transport connection establishment
|
||||
request, // Send the Sentinel request
|
||||
};
|
||||
|
||||
sentinel_action(system::error_code ec) noexcept
|
||||
: type_(type::done)
|
||||
, ec_(ec)
|
||||
{ }
|
||||
|
||||
sentinel_action(const address& addr) noexcept
|
||||
: type_(type::connect)
|
||||
, connect_(&addr)
|
||||
{ }
|
||||
|
||||
static sentinel_action request() { return {type::request}; }
|
||||
|
||||
type get_type() const { return type_; }
|
||||
|
||||
[[nodiscard]]
|
||||
system::error_code error() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == type::done);
|
||||
return ec_;
|
||||
}
|
||||
|
||||
const address& connect_addr() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == type::connect);
|
||||
return *connect_;
|
||||
}
|
||||
|
||||
private:
|
||||
type type_;
|
||||
union {
|
||||
system::error_code ec_;
|
||||
const address* connect_;
|
||||
};
|
||||
|
||||
sentinel_action(type type) noexcept
|
||||
: type_(type)
|
||||
{ }
|
||||
};
|
||||
|
||||
class sentinel_resolve_fsm {
|
||||
int resume_point_{0};
|
||||
std::size_t idx_{0u};
|
||||
|
||||
public:
|
||||
sentinel_resolve_fsm() = default;
|
||||
|
||||
sentinel_action resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr);
|
||||
any_adapter make_sentinel_adapter(connection_state& st);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
35
include/boost/redis/detail/subscription_tracker.hpp
Normal file
35
include/boost/redis/detail/subscription_tracker.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// 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_SUBSCRIPTION_TRACKER_HPP
|
||||
#define BOOST_REDIS_SUBSCRIPTION_TRACKER_HPP
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
class request;
|
||||
|
||||
namespace detail {
|
||||
|
||||
class subscription_tracker {
|
||||
std::set<std::string> channels_;
|
||||
std::set<std::string> pchannels_;
|
||||
|
||||
public:
|
||||
subscription_tracker() = default;
|
||||
void clear();
|
||||
void commit_changes(const request& req);
|
||||
void compose_subscribe_request(request& to) const;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif
|
||||
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,19 +68,19 @@ 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
|
||||
ssl_handshake_timeout,
|
||||
|
||||
/// Can't receive push synchronously without blocking
|
||||
/// (Deprecated) Can't receive push synchronously without blocking
|
||||
sync_receive_push_failed,
|
||||
|
||||
/// 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.
|
||||
@@ -91,6 +91,31 @@ enum class error
|
||||
|
||||
/// 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,
|
||||
|
||||
/// The configuration specified UNIX sockets with Sentinel, which is not supported.
|
||||
sentinel_unix_sockets_unsupported,
|
||||
|
||||
/// No Sentinel could be used to obtain the address of the Redis server.
|
||||
/// Sentinels might be unreachable, have authentication misconfigured or may not know about
|
||||
/// the configured master. Turn logging on for details.
|
||||
sentinel_resolve_failed,
|
||||
|
||||
/// The contacted server is not a master as expected.
|
||||
/// This is likely a transient failure caused by a Sentinel failover in progress.
|
||||
role_check_failed,
|
||||
|
||||
/// Expects a RESP3 string, but got a different data type.
|
||||
expects_resp3_string,
|
||||
|
||||
/// Expects a RESP3 array, but got a different data type.
|
||||
expects_resp3_array,
|
||||
|
||||
/// A @ref basic_connection::async_receive2 operation is already running.
|
||||
/// Only one of such operations might be running at any point in time.
|
||||
already_running,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
218
include/boost/redis/impl/connect_fsm.ipp
Normal file
218
include/boost/redis/impl/connect_fsm.ipp
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// 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 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_, "Connect: hostname resolution failed: ", ec);
|
||||
} else {
|
||||
log_debug(*lgr_, "Connect: hostname resolution 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_, "Connect: TCP connect failed: ", ec);
|
||||
} else {
|
||||
log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint);
|
||||
}
|
||||
|
||||
// Delegate to the regular resume function
|
||||
return resume(ec, 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
|
||||
|
||||
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_, "Connect: UNIX socket connect failed: ", ec);
|
||||
} else {
|
||||
log_debug(*lgr_, "Connect: UNIX socket connect succeeded");
|
||||
}
|
||||
|
||||
// If this failed, we can't continue
|
||||
if (ec) {
|
||||
return ec;
|
||||
}
|
||||
|
||||
// Done
|
||||
return system::error_code();
|
||||
} else {
|
||||
// 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 (st.type == transport_type::tcp_tls && 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 (st.type == transport_type::tcp_tls) {
|
||||
// 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_, "Connect: SSL handshake failed: ", ec);
|
||||
} else {
|
||||
log_debug(*lgr_, "Connect: SSL handshake succeeded");
|
||||
}
|
||||
|
||||
// If this failed, we can't continue
|
||||
if (ec) {
|
||||
return ec;
|
||||
}
|
||||
}
|
||||
|
||||
// Done
|
||||
return system::error_code();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASSERT(false);
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
@@ -51,6 +51,12 @@ void connection::async_exec_impl(
|
||||
impl_.async_exec(req, std::move(adapter), std::move(token));
|
||||
}
|
||||
|
||||
void connection::async_receive2_impl(
|
||||
asio::any_completion_handler<void(boost::system::error_code)> token)
|
||||
{
|
||||
impl_.async_receive2(std::move(token));
|
||||
}
|
||||
|
||||
void connection::cancel(operation op) { impl_.cancel(op); }
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
@@ -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,7 +44,9 @@ 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.";
|
||||
@@ -53,6 +55,22 @@ struct error_category_impl : system::error_category {
|
||||
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.";
|
||||
case error::sentinel_unix_sockets_unsupported:
|
||||
return "The configuration specified UNIX sockets with Sentinel, which is not "
|
||||
"supported.";
|
||||
case error::sentinel_resolve_failed:
|
||||
return "No Sentinel could be used to obtain the address of the Redis server.";
|
||||
case error::role_check_failed:
|
||||
return "The contacted server does not have the expected role. "
|
||||
"This is likely a transient failure caused by a Sentinel failover in progress.";
|
||||
case error::expects_resp3_string:
|
||||
return "Expects a RESP3 string, but got a different data type.";
|
||||
case error::expects_resp3_array:
|
||||
return "Expects a RESP3 array, but got a different data type.";
|
||||
case error::already_running:
|
||||
return "An async_receive2 operation is already running. Only one of such operations "
|
||||
"might be running at any point in time.";
|
||||
default: BOOST_ASSERT(false); return "Boost.Redis error.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef BOOST_REDIS_EXEC_FSM_IPP
|
||||
#define BOOST_REDIS_EXEC_FSM_IPP
|
||||
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/exec_fsm.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
@@ -18,14 +19,20 @@
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t cancel_state)
|
||||
inline bool is_total_cancel(asio::cancellation_type_t type)
|
||||
{
|
||||
return !!(type & asio::cancellation_type_t::total);
|
||||
}
|
||||
|
||||
exec_action exec_fsm::resume(
|
||||
bool connection_is_open,
|
||||
connection_state& st,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
@@ -44,7 +51,7 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, exec_action_type::setup_cancellation)
|
||||
|
||||
// Add the request to the multiplexer
|
||||
mpx_->add(elem_);
|
||||
st.mpx.add(elem_);
|
||||
|
||||
// Notify the writer task that there is work to do. If the task is not
|
||||
// listening (e.g. it's already writing or the connection is not healthy),
|
||||
@@ -58,24 +65,23 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
|
||||
|
||||
// If the request has completed (with error or not), we're done
|
||||
if (elem_->is_done()) {
|
||||
// If the request completed successfully and we were configured to do so,
|
||||
// record the changes applied to the pubsub state
|
||||
if (!elem_->get_error())
|
||||
st.tracker.commit_changes(elem_->get_request());
|
||||
|
||||
// Deallocate memory before finalizing
|
||||
exec_action act{elem_->get_error(), elem_->get_read_size()};
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
elem_.reset();
|
||||
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)) {
|
||||
st.mpx.cancel(elem_);
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
return exec_action{asio::error::operation_aborted};
|
||||
}
|
||||
|
||||
95
include/boost/redis/impl/exec_one_fsm.ipp
Normal file
95
include/boost/redis/impl/exec_one_fsm.ipp
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// 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_EXEC_ONE_FSM_IPP
|
||||
#define BOOST_REDIS_EXEC_ONE_FSM_IPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/exec_one_fsm.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/parser.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 {
|
||||
|
||||
exec_one_action exec_one_fsm::resume(
|
||||
read_buffer& buffer,
|
||||
system::error_code ec,
|
||||
std::size_t bytes_transferred,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Send the request to the server
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, exec_one_action_type::write)
|
||||
|
||||
// Errors and cancellations
|
||||
if (is_terminal_cancel(cancel_state))
|
||||
return system::error_code{asio::error::operation_aborted};
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// If the request didn't expect any response, we're done
|
||||
if (remaining_responses_ == 0u)
|
||||
return system::error_code{};
|
||||
|
||||
// Read responses until we're done
|
||||
buffer.clear();
|
||||
while (true) {
|
||||
// Prepare the buffer to read some data
|
||||
ec = buffer.prepare();
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Read data
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, exec_one_action_type::read_some)
|
||||
|
||||
// Errors and cancellations
|
||||
if (is_terminal_cancel(cancel_state))
|
||||
return system::error_code{asio::error::operation_aborted};
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Commit the data into the buffer
|
||||
buffer.commit(bytes_transferred);
|
||||
|
||||
// Consume the data until we run out or all the responses have been read
|
||||
while (resp3::parse(parser_, buffer.get_commited(), adapter_, ec)) {
|
||||
// Check for errors
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// We've finished parsing a response
|
||||
buffer.consume(parser_.get_consumed());
|
||||
parser_.reset();
|
||||
|
||||
// When no more responses remain, we're done.
|
||||
// Don't read ahead, even if more data is available
|
||||
if (--remaining_responses_ == 0u)
|
||||
return system::error_code{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASSERT(false);
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
248
include/boost/redis/impl/flat_tree.ipp
Normal file
248
include/boost/redis/impl/flat_tree.ipp
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Nikolai Vladimirov (nvladimirov.work@gmail.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/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/throw_exception.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Updates string views by performing pointer arithmetic
|
||||
inline void rebase_strings(view_tree& nodes, const char* old_base, const char* new_base)
|
||||
{
|
||||
for (auto& nd : nodes) {
|
||||
if (!nd.value.empty()) {
|
||||
const auto offset = nd.value.data() - old_base;
|
||||
BOOST_ASSERT(offset >= 0);
|
||||
nd.value = {new_base + offset, nd.value.size()};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Operations in flat_buffer ---
|
||||
|
||||
// Compute the new capacity upon reallocation. We always use powers of 2,
|
||||
// starting in 512, to prevent many small allocations
|
||||
inline std::size_t compute_capacity(std::size_t current, std::size_t requested)
|
||||
{
|
||||
std::size_t res = (std::max)(current, static_cast<std::size_t>(512u));
|
||||
while (res < requested)
|
||||
res *= 2u;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Copy construction
|
||||
inline flat_buffer copy_construct(const flat_buffer& other)
|
||||
{
|
||||
flat_buffer res{{}, other.size, 0u, 0u};
|
||||
|
||||
if (other.size > 0u) {
|
||||
const std::size_t capacity = compute_capacity(0u, other.size);
|
||||
res.data.reset(new char[capacity]);
|
||||
res.capacity = capacity;
|
||||
res.reallocs = 1u;
|
||||
std::copy(other.data.get(), other.data.get() + other.size, res.data.get());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Copy assignment
|
||||
inline void copy_assign(flat_buffer& buff, const flat_buffer& other)
|
||||
{
|
||||
// Make space if required
|
||||
if (buff.capacity < other.size) {
|
||||
const std::size_t capacity = compute_capacity(buff.capacity, other.size);
|
||||
buff.data.reset(new char[capacity]);
|
||||
buff.capacity = capacity;
|
||||
++buff.reallocs;
|
||||
}
|
||||
|
||||
// Copy the contents
|
||||
std::copy(other.data.get(), other.data.get() + other.size, buff.data.get());
|
||||
buff.size = other.size;
|
||||
}
|
||||
|
||||
// Grows the buffer until reaching a target size.
|
||||
// Might rebase the strings in nodes
|
||||
inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes)
|
||||
{
|
||||
if (new_capacity <= buff.capacity)
|
||||
return;
|
||||
|
||||
// Compute the actual capacity that we will be using
|
||||
new_capacity = compute_capacity(buff.capacity, new_capacity);
|
||||
|
||||
// Allocate space
|
||||
std::unique_ptr<char[]> new_buffer{new char[new_capacity]};
|
||||
|
||||
// Copy any data into the newly allocated space
|
||||
const char* data_before = buff.data.get();
|
||||
char* data_after = new_buffer.get();
|
||||
std::copy(data_before, data_before + buff.size, data_after);
|
||||
|
||||
// Update the string views so they don't dangle
|
||||
rebase_strings(nodes, data_before, data_after);
|
||||
|
||||
// Replace the buffer. Note that size hasn't changed here
|
||||
buff.data = std::move(new_buffer);
|
||||
buff.capacity = new_capacity;
|
||||
++buff.reallocs;
|
||||
}
|
||||
|
||||
// Erases the first num_bytes bytes from the buffer by moving
|
||||
// the remaining bytes forward. Rebases the strings in nodes as required.
|
||||
inline void erase_first(flat_buffer& buff, std::size_t num_bytes, view_tree& nodes)
|
||||
{
|
||||
BOOST_ASSERT(num_bytes <= buff.size);
|
||||
if (num_bytes > 0u) {
|
||||
// If we have any data to move, we should always have a buffer
|
||||
BOOST_ASSERT(buff.data.get() != nullptr);
|
||||
|
||||
// Record the old base
|
||||
const char* old_base = buff.data.get() + num_bytes;
|
||||
|
||||
// Move all that we're gonna keep to the start of the buffer
|
||||
auto bytes_left = buff.size - num_bytes;
|
||||
std::memmove(buff.data.get(), old_base, bytes_left);
|
||||
|
||||
// Rebase strings
|
||||
rebase_strings(nodes, old_base, buff.data.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Appends a string to the buffer.
|
||||
// Might rebase the string in nodes, but doesn't append any new node.
|
||||
inline std::string_view append(flat_buffer& buff, std::string_view value, view_tree& nodes)
|
||||
{
|
||||
// If there is nothing to copy, do nothing
|
||||
if (value.empty())
|
||||
return value;
|
||||
|
||||
// Make space for the new string
|
||||
const std::size_t new_size = buff.size + value.size();
|
||||
grow(buff, new_size, nodes);
|
||||
|
||||
// Copy the new value
|
||||
const std::size_t offset = buff.size;
|
||||
std::copy(value.data(), value.data() + value.size(), buff.data.get() + offset);
|
||||
buff.size = new_size;
|
||||
return {buff.data.get() + offset, value.size()};
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
flat_tree::flat_tree(flat_tree const& other)
|
||||
: data_{detail::copy_construct(other.data_)}
|
||||
, view_tree_{other.view_tree_}
|
||||
, total_msgs_{other.total_msgs_}
|
||||
, node_tmp_offset_{other.node_tmp_offset_}
|
||||
, data_tmp_offset_{other.data_tmp_offset_}
|
||||
{
|
||||
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
|
||||
}
|
||||
|
||||
flat_tree& flat_tree::operator=(const flat_tree& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
// Copy the data
|
||||
detail::copy_assign(data_, other.data_);
|
||||
|
||||
// Copy the nodes
|
||||
view_tree_ = other.view_tree_;
|
||||
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
|
||||
|
||||
// Copy the other fields
|
||||
total_msgs_ = other.total_msgs_;
|
||||
node_tmp_offset_ = other.node_tmp_offset_;
|
||||
data_tmp_offset_ = other.data_tmp_offset_;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void flat_tree::reserve(std::size_t bytes, std::size_t nodes)
|
||||
{
|
||||
// Space for the strings
|
||||
detail::grow(data_, bytes, view_tree_);
|
||||
|
||||
// Space for the nodes
|
||||
view_tree_.reserve(nodes);
|
||||
}
|
||||
|
||||
void flat_tree::clear() noexcept
|
||||
{
|
||||
// Discard everything except for the tmp area
|
||||
view_tree_.erase(view_tree_.begin(), view_tree_.begin() + node_tmp_offset_);
|
||||
node_tmp_offset_ = 0u;
|
||||
|
||||
// Do the same for the data area
|
||||
detail::erase_first(data_, data_tmp_offset_, view_tree_);
|
||||
data_tmp_offset_ = 0u;
|
||||
|
||||
// We now have no messages
|
||||
total_msgs_ = 0u;
|
||||
}
|
||||
|
||||
void flat_tree::push(node_view const& nd)
|
||||
{
|
||||
// Add the string
|
||||
const std::string_view str = detail::append(data_, nd.value, view_tree_);
|
||||
|
||||
// Add the node
|
||||
view_tree_.push_back({
|
||||
nd.data_type,
|
||||
nd.aggregate_size,
|
||||
nd.depth,
|
||||
str,
|
||||
});
|
||||
}
|
||||
|
||||
void flat_tree::notify_init()
|
||||
{
|
||||
// Discard any data in the tmp area, as it belongs to an operation that never finished
|
||||
BOOST_ASSERT(node_tmp_offset_ <= view_tree_.size());
|
||||
BOOST_ASSERT(data_tmp_offset_ <= data_.size);
|
||||
view_tree_.resize(node_tmp_offset_);
|
||||
data_.size = data_tmp_offset_;
|
||||
}
|
||||
|
||||
void flat_tree::notify_done()
|
||||
{
|
||||
++total_msgs_;
|
||||
node_tmp_offset_ = view_tree_.size();
|
||||
data_tmp_offset_ = data_.size;
|
||||
}
|
||||
|
||||
const node_view& flat_tree::at(std::size_t i) const
|
||||
{
|
||||
if (i >= size())
|
||||
BOOST_THROW_EXCEPTION(std::out_of_range("flat_tree::at"));
|
||||
return view_tree_[i];
|
||||
}
|
||||
|
||||
bool operator==(flat_tree const& a, flat_tree const& b)
|
||||
{
|
||||
// data is already taken into account by comparing the nodes.
|
||||
// Only committed nodes should be taken into account.
|
||||
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()) &&
|
||||
a.get_total_msgs() == b.get_total_msgs();
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
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
|
||||
110
include/boost/redis/impl/log_utils.hpp
Normal file
110
include/boost/redis/impl/log_utils.hpp
Normal file
@@ -0,0 +1,110 @@
|
||||
/* 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/config.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 <>
|
||||
struct log_traits<address> {
|
||||
static inline void log(std::string& to, const address& value)
|
||||
{
|
||||
to += value.host;
|
||||
to += ':';
|
||||
to += value.port;
|
||||
}
|
||||
};
|
||||
|
||||
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,8 +5,13 @@
|
||||
*/
|
||||
|
||||
#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 {
|
||||
@@ -35,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
|
||||
@@ -60,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();
|
||||
});
|
||||
@@ -76,7 +110,7 @@ void multiplexer::add(std::shared_ptr<elem> const& info)
|
||||
}
|
||||
}
|
||||
|
||||
tribool multiplexer::consume_next_impl(std::string_view data, system::error_code& ec)
|
||||
consume_result multiplexer::consume_impl(system::error_code& ec)
|
||||
{
|
||||
// We arrive here in two states:
|
||||
//
|
||||
@@ -86,34 +120,34 @@ tribool multiplexer::consume_next_impl(std::string_view data, system::error_code
|
||||
// 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(data);
|
||||
|
||||
if (on_push_) {
|
||||
if (!resp3::parse(parser_, data, receive_adapter_, ec))
|
||||
return std::nullopt;
|
||||
return consume_result::needs_more;
|
||||
|
||||
return std::make_optional(true);
|
||||
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_, data, reqs_.front()->get_adapter(), ec))
|
||||
return std::nullopt;
|
||||
return consume_result::needs_more;
|
||||
|
||||
if (ec) {
|
||||
reqs_.front()->notify_error(ec);
|
||||
reqs_.pop_front();
|
||||
return std::make_optional(false);
|
||||
return consume_result::got_response;
|
||||
}
|
||||
|
||||
reqs_.front()->commit_response(parser_.get_consumed());
|
||||
@@ -123,31 +157,48 @@ tribool multiplexer::consume_next_impl(std::string_view data, system::error_code
|
||||
reqs_.pop_front();
|
||||
}
|
||||
|
||||
return std::make_optional(false);
|
||||
return consume_result::got_response;
|
||||
}
|
||||
|
||||
std::pair<tribool, std::size_t> multiplexer::consume_next(
|
||||
std::string_view data,
|
||||
system::error_code& ec)
|
||||
std::pair<consume_result, std::size_t> multiplexer::consume(system::error_code& ec)
|
||||
{
|
||||
auto const ret = consume_next_impl(data, 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.has_value()) {
|
||||
if (ret != consume_result::needs_more) {
|
||||
parser_.reset();
|
||||
commit_usage(ret.value(), consumed);
|
||||
return std::make_pair(ret, consumed);
|
||||
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(std::nullopt, 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()
|
||||
{
|
||||
read_buffer_.clear();
|
||||
write_buffer_.clear();
|
||||
write_offset_ = 0u;
|
||||
parser_.reset();
|
||||
on_push_ = false;
|
||||
cancel_run_called_ = false;
|
||||
@@ -155,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(
|
||||
@@ -164,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);
|
||||
@@ -196,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 {
|
||||
@@ -217,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});
|
||||
});
|
||||
@@ -228,20 +284,20 @@ 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;
|
||||
}
|
||||
|
||||
void multiplexer::commit_usage(bool is_push, std::size_t size)
|
||||
void multiplexer::commit_usage(bool is_push, read_buffer::consume_result res)
|
||||
{
|
||||
if (is_push) {
|
||||
usage_.pushes_received += 1;
|
||||
usage_.push_bytes_received += size;
|
||||
usage_.push_bytes_received += res.consumed;
|
||||
on_push_ = false;
|
||||
} else {
|
||||
usage_.responses_received += 1;
|
||||
usage_.response_bytes_received += size;
|
||||
usage_.response_bytes_received += res.consumed;
|
||||
}
|
||||
|
||||
usage_.bytes_rotated += res.rotated;
|
||||
}
|
||||
|
||||
bool multiplexer::is_next_push(std::string_view data) const noexcept
|
||||
@@ -275,45 +331,37 @@ bool multiplexer::is_next_push(std::string_view data) 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
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
bool multiplexer::is_writing() const noexcept { return !write_buffer_.empty(); }
|
||||
|
||||
void multiplexer::set_receive_adapter(any_adapter adapter)
|
||||
{
|
||||
receive_adapter_ = std::move(adapter);
|
||||
}
|
||||
|
||||
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, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>
|
||||
{
|
||||
return std::make_shared<multiplexer::elem>(req, std::move(adapter));
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
system::error_code read_buffer::prepare_append()
|
||||
system::error_code read_buffer::prepare()
|
||||
{
|
||||
BOOST_ASSERT(append_buf_begin_ == buffer_.size());
|
||||
|
||||
@@ -27,34 +27,32 @@ system::error_code read_buffer::prepare_append()
|
||||
return {};
|
||||
}
|
||||
|
||||
void read_buffer::commit_append(std::size_t read_size)
|
||||
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_append_buffer() noexcept -> span_type
|
||||
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_committed_buffer() const noexcept -> std::string_view
|
||||
auto read_buffer::get_commited() const noexcept -> std::string_view
|
||||
{
|
||||
BOOST_ASSERT(!buffer_.empty());
|
||||
return {buffer_.data(), append_buf_begin_};
|
||||
}
|
||||
|
||||
auto read_buffer::get_committed_size() const noexcept -> std::size_t { return append_buf_begin_; }
|
||||
|
||||
void read_buffer::clear()
|
||||
{
|
||||
buffer_.clear();
|
||||
append_buf_begin_ = 0;
|
||||
}
|
||||
|
||||
std::size_t read_buffer::consume_committed(std::size_t size)
|
||||
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.
|
||||
@@ -62,9 +60,12 @@ std::size_t read_buffer::consume_committed(std::size_t size)
|
||||
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;
|
||||
|
||||
return {size, rotated};
|
||||
}
|
||||
|
||||
void read_buffer::reserve(std::size_t n) { buffer_.reserve(n); }
|
||||
|
||||
@@ -4,70 +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/read_buffer.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(read_buffer& rbuf, multiplexer& mpx) noexcept
|
||||
: read_buffer_{&rbuf}
|
||||
, 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 (;;) {
|
||||
ec = read_buffer_->prepare_append();
|
||||
// Prepare the buffer for the read operation
|
||||
ec = st.mpx.prepare_read();
|
||||
if (ec) {
|
||||
action_after_resume_ = {action::type::done, 0, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
log_debug(st.logger, "Reader task: error in prepare_read: ", ec);
|
||||
return {ec};
|
||||
}
|
||||
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, next_read_type_)
|
||||
read_buffer_->commit_append(bytes_read);
|
||||
// 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_, 4, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
return ec;
|
||||
}
|
||||
|
||||
next_read_type_ = action::type::append_some;
|
||||
while (read_buffer_->get_committed_size() != 0) {
|
||||
res_ = mpx_->consume_next(read_buffer_->get_committed_buffer(), ec);
|
||||
// Process the data that we've read
|
||||
while (st.mpx.get_read_buffer_size() != 0) {
|
||||
res_ = st.mpx.consume(ec);
|
||||
|
||||
if (ec) {
|
||||
// TODO: Perhaps log what has not been consumed to aid
|
||||
// debugging.
|
||||
action_after_resume_ = {action::type::done, res_.second, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 5, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
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;
|
||||
}
|
||||
|
||||
read_buffer_->consume_committed(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);
|
||||
}
|
||||
|
||||
if (res_.first.value()) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 6, action::type::notify_push_receiver, res_.second)
|
||||
// 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
|
||||
@@ -82,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
|
||||
|
||||
85
include/boost/redis/impl/receive_fsm.ipp
Normal file
85
include/boost/redis/impl/receive_fsm.ipp
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Copyright (c) 2018-2026 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/receive_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/experimental/channel_error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
constexpr bool is_any_cancel(asio::cancellation_type_t type)
|
||||
{
|
||||
return !!(
|
||||
type & (asio::cancellation_type_t::terminal | asio::cancellation_type_t::partial |
|
||||
asio::cancellation_type_t::total));
|
||||
}
|
||||
|
||||
// We use the receive2_cancelled flag rather than will_reconnect() to
|
||||
// avoid entanglement between async_run and async_receive2 cancellations.
|
||||
// If we had used will_reconnect(), async_receive2 would be cancelled
|
||||
// when disabling reconnection and async_run exits, and in an unpredictable fashion.
|
||||
receive_action receive_fsm::resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Parallel async_receive2 operations not supported
|
||||
if (st.receive2_running) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, receive_action::action_type::immediate)
|
||||
return system::error_code(error::already_running);
|
||||
}
|
||||
|
||||
// We're now running. Discard any previous cancellation state
|
||||
st.receive2_running = true;
|
||||
st.receive2_cancelled = false;
|
||||
|
||||
// This operation supports total cancellation. Set it up
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, receive_action::action_type::setup_cancellation)
|
||||
|
||||
while (true) {
|
||||
// Wait at least once for a notification to arrive
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, receive_action::action_type::wait)
|
||||
|
||||
// If the wait completed successfully, we have pushes. Drain the channel and exit
|
||||
if (!ec) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, receive_action::action_type::drain_channel)
|
||||
st.receive2_running = false;
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// Check for cancellations
|
||||
if (is_any_cancel(cancel_state) || st.receive2_cancelled) {
|
||||
st.receive2_running = false;
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// If we get any unknown errors, propagate them (shouldn't happen, but just in case)
|
||||
if (ec != asio::experimental::channel_errc::channel_cancelled) {
|
||||
st.receive2_running = false;
|
||||
return ec;
|
||||
}
|
||||
|
||||
// The channel was cancelled and no cancellation state is set.
|
||||
// This is due to a reconnection. Ignore the notification
|
||||
}
|
||||
}
|
||||
|
||||
// We should never get here
|
||||
BOOST_ASSERT(false);
|
||||
return receive_action{system::error_code()};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
@@ -5,7 +5,9 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
@@ -18,7 +20,48 @@ 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
|
||||
|
||||
void boost::redis::request::append(const request& other)
|
||||
{
|
||||
// Remember the old payload size, to update offsets
|
||||
std::size_t old_offset = payload_.size();
|
||||
|
||||
// Add the payload
|
||||
payload_ += other.payload_;
|
||||
commands_ += other.commands_;
|
||||
expected_responses_ += other.expected_responses_;
|
||||
|
||||
// Add the pubsub changes. Offsets need to be updated
|
||||
pubsub_changes_.reserve(pubsub_changes_.size() + other.pubsub_changes_.size());
|
||||
for (const auto& change : other.pubsub_changes_) {
|
||||
pubsub_changes_.push_back({
|
||||
change.type,
|
||||
change.channel_offset + old_offset,
|
||||
change.channel_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void boost::redis::request::add_pubsub_arg(detail::pubsub_change_type type, std::string_view value)
|
||||
{
|
||||
// Add the argument
|
||||
resp3::add_bulk(payload_, value);
|
||||
|
||||
// Track the change.
|
||||
// The final \r\n adds 2 bytes
|
||||
std::size_t offset = payload_.size() - value.size() - 2u;
|
||||
pubsub_changes_.push_back({type, offset, value.size()});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -9,9 +9,13 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
void consume_one(generic_response& r, system::error_code& ec)
|
||||
namespace detail {
|
||||
|
||||
inline void consume_one_impl(generic_response& r, system::error_code& ec)
|
||||
{
|
||||
if (r.has_error())
|
||||
return; // Nothing to consume.
|
||||
@@ -38,10 +42,14 @@ void consume_one(generic_response& r, system::error_code& ec)
|
||||
r.value().erase(std::cbegin(r.value()), match);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
void consume_one(generic_response& r, system::error_code& ec) { detail::consume_one_impl(r, ec); }
|
||||
|
||||
void consume_one(generic_response& r)
|
||||
{
|
||||
system::error_code ec;
|
||||
consume_one(r, ec);
|
||||
detail::consume_one_impl(r, ec);
|
||||
if (ec)
|
||||
throw system::system_error(ec);
|
||||
}
|
||||
|
||||
230
include/boost/redis/impl/run_fsm.ipp
Normal file
230
include/boost/redis/impl/run_fsm.ipp
Normal file
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// 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/connect_params.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/error.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/impl/log_utils.hpp>
|
||||
#include <boost/redis/impl/sentinel_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;
|
||||
if (use_sentinel(cfg))
|
||||
return error::sentinel_unix_sockets_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 on_setup_done(const multiplexer::elem& elm, connection_state& st)
|
||||
{
|
||||
const auto ec = elm.get_error();
|
||||
if (ec) {
|
||||
if (st.diagnostic.empty()) {
|
||||
log_info(st.logger, "Setup request execution: ", ec);
|
||||
} else {
|
||||
log_info(st.logger, "Setup request execution: ", ec, " (", st.diagnostic, ")");
|
||||
}
|
||||
} else {
|
||||
log_info(st.logger, "Setup request execution: success");
|
||||
}
|
||||
}
|
||||
|
||||
inline any_address_view get_server_address(const connection_state& st)
|
||||
{
|
||||
if (st.cfg.unix_socket.empty()) {
|
||||
return {st.cfg.addr, st.cfg.use_ssl};
|
||||
} else {
|
||||
return any_address_view{st.cfg.unix_socket};
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
struct log_traits<any_address_view> {
|
||||
static inline void log(std::string& to, any_address_view value)
|
||||
{
|
||||
if (value.type() == transport_type::unix_socket) {
|
||||
to += '\'';
|
||||
to += value.unix_socket();
|
||||
to += '\'';
|
||||
} else {
|
||||
log_traits<address>::log(to, value.tcp_address());
|
||||
to += value.type() == transport_type::tcp_tls ? " (TLS enabled)" : " (TLS disabled)";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
// Clear any remainder from previous runs
|
||||
st.tracker.clear();
|
||||
|
||||
// Compose the PING request. This only depends on the config, so it can be done just once
|
||||
compose_ping_request(st.cfg, st.ping_req);
|
||||
|
||||
if (use_sentinel(st.cfg)) {
|
||||
// Sentinel request. Same as above
|
||||
compose_sentinel_request(st.cfg);
|
||||
|
||||
// Bootstrap the sentinel list with the ones configured by the user
|
||||
st.sentinels = st.cfg.sentinel.addresses;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// Sentinel resolve, if required. This leaves the address in st.cfg.address
|
||||
if (use_sentinel(st.cfg)) {
|
||||
// This operation does the logging for us.
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::sentinel_resolve)
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Run: cancelled (4)");
|
||||
return {asio::error::operation_aborted};
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec)
|
||||
goto sleep_and_reconnect;
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
log_info(st.logger, "Trying to connect to Redis server at ", get_server_address(st));
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, 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 (ec) {
|
||||
// There was an error. Skip to the reconnection loop
|
||||
log_info(
|
||||
st.logger,
|
||||
"Failed to connect to Redis server at ",
|
||||
get_server_address(st),
|
||||
": ",
|
||||
ec);
|
||||
goto sleep_and_reconnect;
|
||||
}
|
||||
|
||||
// We were successful
|
||||
log_info(st.logger, "Connected to Redis server at ", get_server_address(st));
|
||||
|
||||
// Initialization
|
||||
st.mpx.reset();
|
||||
st.diagnostic.clear();
|
||||
compose_setup_request(st.cfg, st.tracker, st.setup_req);
|
||||
|
||||
// Add the setup request to the multiplexer
|
||||
if (st.setup_req.get_commands() != 0u) {
|
||||
auto elm = make_elem(st.setup_req, make_any_adapter_impl(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_, 5, 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_, 6, 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);
|
||||
}
|
||||
|
||||
sleep_and_reconnect:
|
||||
|
||||
// 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_, 7, 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();
|
||||
}
|
||||
|
||||
connect_params make_run_connect_params(const connection_state& st)
|
||||
{
|
||||
return {
|
||||
get_server_address(st),
|
||||
st.cfg.resolve_timeout,
|
||||
st.cfg.connect_timeout,
|
||||
st.cfg.ssl_handshake_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
182
include/boost/redis/impl/sentinel_resolve_fsm.ipp
Normal file
182
include/boost/redis/impl/sentinel_resolve_fsm.ipp
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// 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_SENTINEL_RESOLVE_FSM_IPP
|
||||
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_IPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/impl/log_utils.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Logs an error at info level, and also stores it in the state,
|
||||
// so it can be logged at error level if all Sentinels fail.
|
||||
template <class... Args>
|
||||
void log_sentinel_error(connection_state& st, std::size_t current_idx, const Args&... args)
|
||||
{
|
||||
st.diagnostic += "\n ";
|
||||
std::size_t size_before = st.diagnostic.size();
|
||||
format_log_args(st.diagnostic, "Sentinel at ", st.sentinels[current_idx], ": ", args...);
|
||||
log_info(st.logger, std::string_view{st.diagnostic}.substr(size_before));
|
||||
}
|
||||
|
||||
sentinel_action sentinel_resolve_fsm::resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
st.diagnostic.clear();
|
||||
|
||||
log_info(
|
||||
st.logger,
|
||||
"Trying to resolve the address of ",
|
||||
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
|
||||
" '",
|
||||
st.cfg.sentinel.master_name,
|
||||
"' using Sentinel");
|
||||
|
||||
// Try all Sentinels in order. Upon any errors, save the diagnostic and try with the next one.
|
||||
// If none of them are available, print an error diagnostic and fail.
|
||||
for (idx_ = 0u; idx_ < st.sentinels.size(); ++idx_) {
|
||||
log_debug(st.logger, "Trying to contact Sentinel at ", st.sentinels[idx_]);
|
||||
|
||||
// Try to connect
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, st.sentinels[idx_])
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Sentinel resolve: cancelled (1)");
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec) {
|
||||
log_sentinel_error(st, idx_, "connection establishment error: ", ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute the Sentinel request
|
||||
log_debug(st.logger, "Executing Sentinel request at ", st.sentinels[idx_]);
|
||||
st.sentinel_resp_nodes.clear();
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, sentinel_action::request())
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Sentinel resolve: cancelled (2)");
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec) {
|
||||
log_sentinel_error(st, idx_, "error while executing request: ", ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
sentinel_response resp;
|
||||
ec = parse_sentinel_response(st.sentinel_resp_nodes, st.cfg.sentinel.server_role, resp);
|
||||
|
||||
if (ec) {
|
||||
if (ec == error::resp3_simple_error || ec == error::resp3_blob_error) {
|
||||
log_sentinel_error(st, idx_, "responded with an error: ", resp.diagnostic);
|
||||
} else if (ec == error::resp3_null) {
|
||||
log_sentinel_error(st, idx_, "doesn't know about the configured master");
|
||||
} else {
|
||||
log_sentinel_error(
|
||||
st,
|
||||
idx_,
|
||||
"error parsing response (maybe forgot to upgrade to RESP3?): ",
|
||||
ec);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// When asking for replicas, we might get no replicas
|
||||
if (st.cfg.sentinel.server_role == role::replica && resp.replicas.empty()) {
|
||||
log_sentinel_error(st, idx_, "the configured master has no replicas");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store the resulting address in a well-known place
|
||||
if (st.cfg.sentinel.server_role == role::master) {
|
||||
st.cfg.addr = resp.master_addr;
|
||||
} else {
|
||||
// Choose a random replica
|
||||
std::uniform_int_distribution<std::size_t> dist{0u, resp.replicas.size() - 1u};
|
||||
const auto idx = dist(st.eng.get());
|
||||
st.cfg.addr = resp.replicas[idx];
|
||||
}
|
||||
|
||||
// Sentinel knows about this master. Log and update our config
|
||||
log_info(
|
||||
st.logger,
|
||||
"Sentinel at ",
|
||||
st.sentinels[idx_],
|
||||
" resolved the server address to ",
|
||||
st.cfg.addr);
|
||||
|
||||
update_sentinel_list(st.sentinels, idx_, resp.sentinels, st.cfg.sentinel.addresses);
|
||||
|
||||
st.sentinel_resp_nodes.clear(); // reduce memory consumption
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// No Sentinel resolved our address
|
||||
log_err(
|
||||
st.logger,
|
||||
"Failed to resolve the address of ",
|
||||
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
|
||||
" '",
|
||||
st.cfg.sentinel.master_name,
|
||||
"'. Tried the following Sentinels:",
|
||||
st.diagnostic);
|
||||
return {error::sentinel_resolve_failed};
|
||||
}
|
||||
|
||||
// We should never get here
|
||||
BOOST_ASSERT(false);
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
connect_params make_sentinel_connect_params(const config& cfg, const address& addr)
|
||||
{
|
||||
return {
|
||||
any_address_view{addr, cfg.sentinel.use_ssl},
|
||||
cfg.sentinel.resolve_timeout,
|
||||
cfg.sentinel.connect_timeout,
|
||||
cfg.sentinel.ssl_handshake_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
any_adapter make_sentinel_adapter(connection_state& st)
|
||||
{
|
||||
return any_adapter(st.sentinel_resp_nodes);
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
277
include/boost/redis/impl/sentinel_utils.hpp
Normal file
277
include/boost/redis/impl/sentinel_utils.hpp
Normal file
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// 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_SENTINEL_UTILS_HPP
|
||||
#define BOOST_REDIS_SENTINEL_UTILS_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/core/span.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Returns true if Sentinel should be used
|
||||
inline bool use_sentinel(const config& cfg) { return !cfg.sentinel.addresses.empty(); }
|
||||
|
||||
// Composes the request to send to Sentinel modifying cfg.sentinel.setup
|
||||
inline void compose_sentinel_request(config& cfg)
|
||||
{
|
||||
// These commands should go after the user-supplied setup, as this might involve authentication.
|
||||
// We ask for the master even when connecting to replicas to correctly detect when the master doesn't exist
|
||||
cfg.sentinel.setup.push("SENTINEL", "GET-MASTER-ADDR-BY-NAME", cfg.sentinel.master_name);
|
||||
if (cfg.sentinel.server_role == role::replica)
|
||||
cfg.sentinel.setup.push("SENTINEL", "REPLICAS", cfg.sentinel.master_name);
|
||||
cfg.sentinel.setup.push("SENTINEL", "SENTINELS", cfg.sentinel.master_name);
|
||||
|
||||
// Note that we don't care about request flags because this is a one-time request
|
||||
}
|
||||
|
||||
// Parses a list of replicas or sentinels
|
||||
inline system::error_code parse_server_list(
|
||||
const resp3::node*& first,
|
||||
const resp3::node* last,
|
||||
std::vector<address>& out)
|
||||
{
|
||||
const auto* it = first;
|
||||
ignore_unused(last);
|
||||
|
||||
// The root node must be an array
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 0u);
|
||||
if (it->data_type != resp3::type::array)
|
||||
return {error::expects_resp3_array};
|
||||
const std::size_t num_servers = it->aggregate_size;
|
||||
++it;
|
||||
|
||||
// Each element in the array represents a server
|
||||
out.resize(num_servers);
|
||||
for (std::size_t i = 0u; i < num_servers; ++i) {
|
||||
// A server is a map (resp3) or array (resp2, currently unsupported)
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::map)
|
||||
return {error::expects_resp3_map};
|
||||
const std::size_t num_key_values = it->aggregate_size;
|
||||
++it;
|
||||
|
||||
// The server object is composed by a set of key/value pairs.
|
||||
// Skip everything except for the ones we care for.
|
||||
bool ip_seen = false, port_seen = false;
|
||||
for (std::size_t j = 0; j < num_key_values; ++j) {
|
||||
// Key. It should be a string
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 2u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
const std::string_view key = it->value;
|
||||
++it;
|
||||
|
||||
// Value. All values seem to be strings, too.
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 2u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
|
||||
// Record it
|
||||
if (key == "ip") {
|
||||
ip_seen = true;
|
||||
out[i].host = it->value;
|
||||
} else if (key == "port") {
|
||||
port_seen = true;
|
||||
out[i].port = it->value;
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
// Check that the response actually contained the fields we wanted
|
||||
if (!ip_seen || !port_seen)
|
||||
return {error::empty_field};
|
||||
}
|
||||
|
||||
// Done
|
||||
first = it;
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// The output type of parse_sentinel_response
|
||||
struct sentinel_response {
|
||||
std::string diagnostic; // In case the server returned an error
|
||||
address master_addr; // Always populated
|
||||
std::vector<address> replicas; // Populated only when connecting to replicas
|
||||
std::vector<address> sentinels;
|
||||
};
|
||||
|
||||
// Parses an array of nodes into a sentinel_response.
|
||||
// The request originating this response should be:
|
||||
// <user-supplied commands, as per sentinel_config::setup>
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
// SENTINEL REPLICAS (only if server_role is replica)
|
||||
// SENTINEL SENTINELS
|
||||
// SENTINEL SENTINELS and SENTINEL REPLICAS error when the master name is unknown. Error nodes
|
||||
// should be allowed in the node array.
|
||||
// This means that we can't use generic_response, since its adapter errors on error nodes.
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME is sent even when connecting to replicas
|
||||
// for better diagnostics when the master name is unknown.
|
||||
// Preconditions:
|
||||
// * There are at least 2 (master)/3 (replica) root nodes.
|
||||
// * The node array originates from parsing a valid RESP3 message.
|
||||
// E.g. we won't check that the first node has depth 0.
|
||||
inline system::error_code parse_sentinel_response(
|
||||
span<const resp3::node> nodes,
|
||||
role server_role,
|
||||
sentinel_response& out)
|
||||
{
|
||||
auto check_errors = [&out](const resp3::node& nd) {
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::simple_error:
|
||||
out.diagnostic = nd.value;
|
||||
return system::error_code(error::resp3_simple_error);
|
||||
case resp3::type::blob_error:
|
||||
out.diagnostic = nd.value;
|
||||
return system::error_code(error::resp3_blob_error);
|
||||
default: return system::error_code();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear the output
|
||||
out.diagnostic.clear();
|
||||
out.sentinels.clear();
|
||||
out.replicas.clear();
|
||||
|
||||
// Find the first root node of interest. It's the 2nd or 3rd, starting with the end
|
||||
auto find_first = [nodes, server_role] {
|
||||
const std::size_t expected_roots = server_role == role::master ? 2u : 3u;
|
||||
std::size_t roots_seen = 0u;
|
||||
for (auto it = nodes.rbegin();; ++it) {
|
||||
BOOST_ASSERT(it != nodes.rend());
|
||||
if (it->depth == 0u && ++roots_seen == expected_roots)
|
||||
return &*it;
|
||||
}
|
||||
};
|
||||
const resp3::node* lib_first = find_first();
|
||||
|
||||
// Iterators
|
||||
const resp3::node* it = nodes.begin();
|
||||
const resp3::node* last = nodes.end();
|
||||
ignore_unused(last);
|
||||
|
||||
// Go through all the responses to user-supplied requests checking for errors
|
||||
for (; it != lib_first; ++it) {
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
}
|
||||
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
|
||||
// Check for errors
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// If the root node is NULL, Sentinel doesn't know about this master.
|
||||
// We use resp3_null to signal this fact. This doesn't reach the end user.
|
||||
if (it->data_type == resp3::type::null) {
|
||||
return {error::resp3_null};
|
||||
}
|
||||
|
||||
// If the root node is an array, an IP and port follow
|
||||
if (it->data_type != resp3::type::array)
|
||||
return {error::expects_resp3_array};
|
||||
if (it->aggregate_size != 2u)
|
||||
return {error::incompatible_size};
|
||||
++it;
|
||||
|
||||
// IP
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
out.master_addr.host = it->value;
|
||||
++it;
|
||||
|
||||
// Port
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
out.master_addr.port = it->value;
|
||||
++it;
|
||||
|
||||
if (server_role == role::replica) {
|
||||
// SENTINEL REPLICAS
|
||||
|
||||
// This request fails if Sentinel doesn't know about this master.
|
||||
// However, that's not the case if we got here.
|
||||
// Check for other errors.
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// Actual parsing
|
||||
if (auto ec = parse_server_list(it, last, out.replicas))
|
||||
return ec;
|
||||
}
|
||||
|
||||
// SENTINEL SENTINELS
|
||||
|
||||
// This request fails if Sentinel doesn't know about this master.
|
||||
// However, that's not the case if we got here.
|
||||
// Check for other errors.
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// Actual parsing
|
||||
if (auto ec = parse_server_list(it, last, out.sentinels))
|
||||
return ec;
|
||||
|
||||
// Done
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// Updates the internal Sentinel list.
|
||||
// to should never be empty
|
||||
inline void update_sentinel_list(
|
||||
std::vector<address>& to,
|
||||
std::size_t current_index, // the one to maintain and place first
|
||||
span<const address> gossip_sentinels, // the ones that SENTINEL SENTINELS returned
|
||||
span<const address> bootstrap_sentinels // the ones the user supplied
|
||||
)
|
||||
{
|
||||
BOOST_ASSERT(!to.empty());
|
||||
|
||||
// Remove everything, except the Sentinel that succeeded
|
||||
if (current_index != 0u)
|
||||
std::swap(to.front(), to[current_index]);
|
||||
to.resize(1u);
|
||||
|
||||
// Add one group. These Sentinels are always unique and don't include the one we're currently connected to.
|
||||
to.insert(to.end(), gossip_sentinels.begin(), gossip_sentinels.end());
|
||||
|
||||
// Insert any user-supplied sentinels, if not already present.
|
||||
// This is O(n^2), but is okay because n will be small.
|
||||
// The list can't be sorted, anyway
|
||||
for (const auto& sentinel : bootstrap_sentinels) {
|
||||
if (std::find(to.begin(), to.end(), sentinel) == to.end())
|
||||
to.push_back(sentinel);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
133
include/boost/redis/impl/setup_request_utils.hpp
Normal file
133
include/boost/redis/impl/setup_request_utils.hpp
Normal file
@@ -0,0 +1,133 @@
|
||||
/* 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/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/subscription_tracker.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp> // use_sentinel
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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(
|
||||
const config& cfg,
|
||||
const subscription_tracker& pubsub_st,
|
||||
request& req)
|
||||
{
|
||||
// Clear any previous contents
|
||||
req.clear();
|
||||
|
||||
// Set the appropriate flags
|
||||
request_access::set_priority(req, true);
|
||||
req.get_config().cancel_if_unresponded = true;
|
||||
req.get_config().cancel_on_connection_lost = true;
|
||||
|
||||
if (cfg.use_setup) {
|
||||
// We should use the provided request as-is
|
||||
req.append(cfg.setup);
|
||||
} else {
|
||||
// We're not using the setup request as-is, but should compose one based on
|
||||
// the values passed by the user
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// When using Sentinel, we should add a role check.
|
||||
// This must happen after the other commands, as it requires authentication.
|
||||
if (use_sentinel(cfg))
|
||||
req.push("ROLE");
|
||||
|
||||
// Add any subscription commands require to restore the PubSub state
|
||||
pubsub_st.compose_subscribe_request(req);
|
||||
}
|
||||
|
||||
class setup_adapter {
|
||||
connection_state* st_;
|
||||
std::size_t response_idx_{0u};
|
||||
bool role_seen_{false};
|
||||
|
||||
system::error_code on_node_impl(const resp3::node_view& nd)
|
||||
{
|
||||
// An error node is always an error
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::simple_error:
|
||||
case resp3::type::blob_error: st_->diagnostic = nd.value; return error::resp3_hello;
|
||||
default: ;
|
||||
}
|
||||
|
||||
// When using Sentinel, we add a ROLE command at the end.
|
||||
// We need to ensure that this instance is a master.
|
||||
// ROLE may be followed by subscribe requests, but these don't expect any response.
|
||||
if (use_sentinel(st_->cfg) && response_idx_ == st_->setup_req.get_expected_responses() - 1u) {
|
||||
// ROLE's response should be an array of at least 1 element
|
||||
if (nd.depth == 0u) {
|
||||
if (nd.data_type != resp3::type::array)
|
||||
return error::invalid_data_type;
|
||||
if (nd.aggregate_size == 0u)
|
||||
return error::incompatible_size;
|
||||
}
|
||||
|
||||
// The first node should be 'master' if we're connecting to a primary,
|
||||
// 'slave' if we're connecting to a replica
|
||||
if (nd.depth == 1u && !role_seen_) {
|
||||
role_seen_ = true;
|
||||
if (nd.data_type != resp3::type::blob_string)
|
||||
return error::invalid_data_type;
|
||||
|
||||
const char* expected_role = st_->cfg.sentinel.server_role == role::master ? "master"
|
||||
: "slave";
|
||||
if (nd.value != expected_role)
|
||||
return error::role_check_failed;
|
||||
}
|
||||
}
|
||||
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
public:
|
||||
explicit setup_adapter(connection_state& st) noexcept
|
||||
: st_(&st)
|
||||
{ }
|
||||
|
||||
void on_init() { }
|
||||
void on_done() { ++response_idx_; }
|
||||
void on_node(const resp3::node_view& node, system::error_code& ec) { ec = on_node_impl(node); }
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_RUNNER_HPP
|
||||
44
include/boost/redis/impl/subscription_tracker.ipp
Normal file
44
include/boost/redis/impl/subscription_tracker.ipp
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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/subscription_tracker.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
void subscription_tracker::clear()
|
||||
{
|
||||
channels_.clear();
|
||||
pchannels_.clear();
|
||||
}
|
||||
|
||||
void subscription_tracker::commit_changes(const request& req)
|
||||
{
|
||||
for (const auto& ch : request_access::pubsub_changes(req)) {
|
||||
std::string channel{req.payload().substr(ch.channel_offset, ch.channel_size)};
|
||||
switch (ch.type) {
|
||||
case pubsub_change_type::subscribe: channels_.insert(std::move(channel)); break;
|
||||
case pubsub_change_type::unsubscribe: channels_.erase(std::move(channel)); break;
|
||||
case pubsub_change_type::psubscribe: pchannels_.insert(std::move(channel)); break;
|
||||
case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(channel)); break;
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subscription_tracker::compose_subscribe_request(request& to) const
|
||||
{
|
||||
to.push_range("SUBSCRIBE", channels_);
|
||||
to.push_range("PSUBSCRIBE", pchannels_);
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
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,73 @@ 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.
|
||||
|
||||
/**
|
||||
* @brief (Deprecated) Refers to `async_receive` and `async_receive2` operations.
|
||||
*
|
||||
* To cancel `async_receive2`, use either @ref basic_connection::cancel with no arguments
|
||||
* or per-operation cancellation.
|
||||
*/
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
// NOTE: For some commands like hset it would be a good idea to assert
|
||||
// the value type is a pair.
|
||||
@@ -21,7 +24,23 @@ namespace boost::redis {
|
||||
|
||||
namespace detail {
|
||||
auto has_response(std::string_view cmd) -> bool;
|
||||
}
|
||||
struct request_access;
|
||||
|
||||
enum class pubsub_change_type
|
||||
{
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
psubscribe,
|
||||
punsubscribe,
|
||||
};
|
||||
|
||||
struct pubsub_change {
|
||||
pubsub_change_type type;
|
||||
std::size_t channel_offset;
|
||||
std::size_t channel_size;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** @brief Represents a Redis request.
|
||||
*
|
||||
@@ -33,11 +52,9 @@ auto has_response(std::string_view cmd) -> bool;
|
||||
*
|
||||
* @code
|
||||
* request r;
|
||||
* r.push("HELLO", 3);
|
||||
* r.push("FLUSHALL");
|
||||
* r.push("PING");
|
||||
* r.push("PING", "key");
|
||||
* r.push("QUIT");
|
||||
* r.push("SET", "k1", "some_value");
|
||||
* r.push("SET", "k2", "other_value");
|
||||
* r.push("GET", "k3");
|
||||
* @endcode
|
||||
*
|
||||
* Uses a `std::string` for internal storage.
|
||||
@@ -46,31 +63,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 +114,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 +129,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_;
|
||||
}
|
||||
@@ -103,6 +142,7 @@ public:
|
||||
void clear()
|
||||
{
|
||||
payload_.clear();
|
||||
pubsub_changes_.clear();
|
||||
commands_ = 0;
|
||||
expected_responses_ = 0;
|
||||
has_hello_priority_ = false;
|
||||
@@ -123,14 +163,14 @@ public:
|
||||
*
|
||||
* @code
|
||||
* request req;
|
||||
* req.push("SET", "key", "some string", "EX", "2");
|
||||
* req.push("SET", "key", "some string", "EX", 2);
|
||||
* @endcode
|
||||
*
|
||||
* This will add a `SET` command with value `"some string"` and an
|
||||
* expiration of 2 seconds.
|
||||
*
|
||||
* Command arguments should either be convertible to `std::string_view`
|
||||
* or support the `boost_redis_to_bulk` function.
|
||||
* Command arguments should either be convertible to `std::string_view`,
|
||||
* integral types, or support the `boost_redis_to_bulk` function.
|
||||
* This function is a customization point that must be made available
|
||||
* using ADL and must have the following signature:
|
||||
*
|
||||
@@ -142,7 +182,7 @@ public:
|
||||
* See cpp20_serialization.cpp
|
||||
*
|
||||
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
|
||||
* @param args Command arguments. Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument.
|
||||
* @param args Command arguments. `args` is allowed to be empty.
|
||||
* @tparam Ts Types of the command arguments.
|
||||
*
|
||||
*/
|
||||
@@ -173,21 +213,36 @@ public:
|
||||
* req.push_range("HSET", "key", map.cbegin(), map.cend());
|
||||
* @endcode
|
||||
*
|
||||
* Command arguments should either be convertible to `std::string_view`
|
||||
* or support the `boost_redis_to_bulk` function.
|
||||
* This function is a customization point that must be made available
|
||||
* using ADL and must have the following signature:
|
||||
* This will generate the following command:
|
||||
*
|
||||
* @code
|
||||
* void boost_redis_to_bulk(std::string& to, T const& t);
|
||||
* HSET key key1 value1 key2 value2 key3 value3
|
||||
* @endcode
|
||||
*
|
||||
*
|
||||
* *If the passed range is empty, no command is added* and this
|
||||
* function becomes a no-op.
|
||||
*
|
||||
* The value type of the passed range should satisfy one of the following:
|
||||
*
|
||||
* @li The type is convertible to `std::string_view`. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type is an integral type. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
|
||||
* per element in the range. This function is a customization point that must be made available
|
||||
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
|
||||
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
|
||||
* the points above. Two arguments are added per element in the range.
|
||||
* Nested pairs are not allowed.
|
||||
* @li The type is a `std::tuple` instantiation, with every argument supporting
|
||||
* one of the points above. N arguments are added per element in the range,
|
||||
* with N being the tuple size. Nested tuples are not allowed.
|
||||
*
|
||||
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
|
||||
* @param key The command key. It will be added as the first argument to the command.
|
||||
* @param begin Iterator to the begin of the range.
|
||||
* @param end Iterator to the end of the range.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view`
|
||||
* or supports `boost_redis_to_bulk`.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
|
||||
*
|
||||
* See cpp20_serialization.cpp
|
||||
*/
|
||||
@@ -222,27 +277,42 @@ public:
|
||||
* of arguments and don't have a key. For example:
|
||||
*
|
||||
* @code
|
||||
* std::set<std::string> channels
|
||||
* { "channel1" , "channel2" , "channel3" };
|
||||
* std::set<std::string> keys
|
||||
* { "key1" , "key2" , "key3" };
|
||||
*
|
||||
* request req;
|
||||
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
|
||||
* req.push("MGET", keys.begin(), keys.end());
|
||||
* @endcode
|
||||
*
|
||||
* Command arguments should either be convertible to `std::string_view`
|
||||
* or support the `boost_redis_to_bulk` function.
|
||||
* This function is a customization point that must be made available
|
||||
* using ADL and must have the following signature:
|
||||
* This will generate the following command:
|
||||
*
|
||||
* @code
|
||||
* void boost_redis_to_bulk(std::string& to, T const& t);
|
||||
* MGET key1 key2 key3
|
||||
* @endcode
|
||||
*
|
||||
* *If the passed range is empty, no command is added* and this
|
||||
* function becomes a no-op.
|
||||
*
|
||||
* The value type of the passed range should satisfy one of the following:
|
||||
*
|
||||
* @li The type is convertible to `std::string_view`. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type is an integral type. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
|
||||
* per element in the range. This function is a customization point that must be made available
|
||||
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
|
||||
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
|
||||
* the points above. Two arguments are added per element in the range.
|
||||
* Nested pairs are not allowed.
|
||||
* @li The type is a `std::tuple` instantiation, with every argument supporting
|
||||
* one of the points above. N arguments are added per element in the range,
|
||||
* with N being the tuple size. Nested tuples are not allowed.
|
||||
*
|
||||
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
|
||||
* @param begin Iterator to the begin of the range.
|
||||
* @param end Iterator to the end of the range.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view`
|
||||
* or supports `boost_redis_to_bulk`.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
|
||||
*
|
||||
* See cpp20_serialization.cpp
|
||||
*/
|
||||
@@ -273,13 +343,31 @@ public:
|
||||
*
|
||||
* Equivalent to the overload taking a range of begin and end
|
||||
* iterators.
|
||||
*
|
||||
* *If the passed range is empty, no command is added* and this
|
||||
* function becomes a no-op.
|
||||
*
|
||||
* The value type of the passed range should satisfy one of the following:
|
||||
*
|
||||
* @li The type is convertible to `std::string_view`. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type is an integral type. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
|
||||
* per element in the range. This function is a customization point that must be made available
|
||||
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
|
||||
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
|
||||
* the points above. Two arguments are added per element in the range.
|
||||
* Nested pairs are not allowed.
|
||||
* @li The type is a `std::tuple` instantiation, with every argument supporting
|
||||
* one of the points above. N arguments are added per element in the range,
|
||||
* with N being the tuple size. Nested tuples are not allowed.
|
||||
*
|
||||
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
|
||||
* @param key The command key. It will be added as the first argument to the command.
|
||||
* @param range Range containing the command arguments.
|
||||
* @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain
|
||||
* iterators. The range elements should be convertible to `std::string_view`
|
||||
* or support `boost_redis_to_bulk`.
|
||||
* iterators.
|
||||
*/
|
||||
template <class Range>
|
||||
void push_range(
|
||||
@@ -297,12 +385,30 @@ public:
|
||||
*
|
||||
* Equivalent to the overload taking a range of begin and end
|
||||
* iterators.
|
||||
*
|
||||
* *If the passed range is empty, no command is added* and this
|
||||
* function becomes a no-op.
|
||||
*
|
||||
* The value type of the passed range should satisfy one of the following:
|
||||
*
|
||||
* @li The type is convertible to `std::string_view`. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type is an integral type. One argument is added
|
||||
* per element in the range.
|
||||
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
|
||||
* per element in the range. This function is a customization point that must be made available
|
||||
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
|
||||
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
|
||||
* the points above. Two arguments are added per element in the range.
|
||||
* Nested pairs are not allowed.
|
||||
* @li The type is a `std::tuple` instantiation, with every argument supporting
|
||||
* one of the points above. N arguments are added per element in the range,
|
||||
* with N being the tuple size. Nested tuples are not allowed.
|
||||
*
|
||||
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
|
||||
* @param range Range containing the command arguments.
|
||||
* @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain
|
||||
* iterators. The range elements should be convertible to `std::string_view`
|
||||
* or support `boost_redis_to_bulk`.
|
||||
* iterators.
|
||||
*/
|
||||
template <class Range>
|
||||
void push_range(
|
||||
@@ -315,6 +421,309 @@ public:
|
||||
push_range(cmd, cbegin(range), cend(range));
|
||||
}
|
||||
|
||||
/** @brief Appends the commands in another request to the end of the request.
|
||||
*
|
||||
* Appends all the commands contained in `other` to the end of
|
||||
* this request. Configuration flags in `*this`,
|
||||
* like @ref config::cancel_if_unresponded, are *not* modified,
|
||||
* even if `other` has a different config than `*this`.
|
||||
*
|
||||
* @param other The request containing the commands to append.
|
||||
*/
|
||||
void append(const request& other);
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `{"ch1", "ch2"}`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void subscribe(std::initializer_list<std::string_view> channels)
|
||||
{
|
||||
subscribe(channels.begin(), channels.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void subscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr)
|
||||
{
|
||||
subscribe(std::cbegin(channels), std::cend(channels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`channels_begin`, `channels_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void subscribe(ForwardIt channels_begin, ForwardIt channels_end)
|
||||
{
|
||||
push_pubsub("SUBSCRIBE", detail::pubsub_change_type::subscribe, channels_begin, channels_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `{"ch1", "ch2"}`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void unsubscribe(std::initializer_list<std::string_view> channels)
|
||||
{
|
||||
unsubscribe(channels.begin(), channels.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void unsubscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr)
|
||||
{
|
||||
unsubscribe(std::cbegin(channels), std::cend(channels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`channels_begin`, `channels_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void unsubscribe(ForwardIt channels_begin, ForwardIt channels_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"UNSUBSCRIBE",
|
||||
detail::pubsub_change_type::unsubscribe,
|
||||
channels_begin,
|
||||
channels_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `{"news.*", "events.*"}`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void psubscribe(std::initializer_list<std::string_view> patterns)
|
||||
{
|
||||
psubscribe(patterns.begin(), patterns.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void psubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr)
|
||||
{
|
||||
psubscribe(std::cbegin(patterns), std::cend(patterns));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`patterns_begin`, `patterns_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void psubscribe(ForwardIt patterns_begin, ForwardIt patterns_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"PSUBSCRIBE",
|
||||
detail::pubsub_change_type::psubscribe,
|
||||
patterns_begin,
|
||||
patterns_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `{"news.*", "events.*"}`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void punsubscribe(std::initializer_list<std::string_view> patterns)
|
||||
{
|
||||
punsubscribe(patterns.begin(), patterns.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void punsubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr)
|
||||
{
|
||||
punsubscribe(std::cbegin(patterns), std::cend(patterns));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`patterns_begin`, `patterns_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void punsubscribe(ForwardIt patterns_begin, ForwardIt patterns_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"PUNSUBSCRIBE",
|
||||
detail::pubsub_change_type::punsubscribe,
|
||||
patterns_begin,
|
||||
patterns_end);
|
||||
}
|
||||
|
||||
private:
|
||||
void check_cmd(std::string_view cmd)
|
||||
{
|
||||
@@ -332,8 +741,55 @@ private:
|
||||
std::size_t commands_ = 0;
|
||||
std::size_t expected_responses_ = 0;
|
||||
bool has_hello_priority_ = false;
|
||||
std::vector<detail::pubsub_change> pubsub_changes_{};
|
||||
|
||||
void add_pubsub_arg(detail::pubsub_change_type type, std::string_view value);
|
||||
|
||||
template <class ForwardIt>
|
||||
void push_pubsub(
|
||||
std::string_view cmd,
|
||||
detail::pubsub_change_type type,
|
||||
ForwardIt channels_begin,
|
||||
ForwardIt channels_end)
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible_v<
|
||||
typename std::iterator_traits<ForwardIt>::value_type,
|
||||
std::string_view>,
|
||||
"subscribe, psubscribe, unsubscribe and punsubscribe should be passed ranges of elements "
|
||||
"convertible to std::string_view");
|
||||
if (channels_begin == channels_end)
|
||||
return;
|
||||
|
||||
auto const distance = std::distance(channels_begin, channels_end);
|
||||
resp3::add_header(payload_, resp3::type::array, 1 + distance);
|
||||
resp3::add_bulk(payload_, cmd);
|
||||
|
||||
for (; channels_begin != channels_end; ++channels_begin)
|
||||
add_pubsub_arg(type, *channels_begin);
|
||||
|
||||
++commands_; // these commands don't have a response
|
||||
}
|
||||
|
||||
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_; }
|
||||
inline static const std::vector<detail::pubsub_change>& pubsub_changes(const request& r)
|
||||
{
|
||||
return r.pubsub_changes_;
|
||||
}
|
||||
};
|
||||
|
||||
// Creates a HELLO 3 request
|
||||
request make_hello_request();
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_REQUEST_HPP
|
||||
|
||||
403
include/boost/redis/resp3/flat_tree.hpp
Normal file
403
include/boost/redis/resp3/flat_tree.hpp
Normal file
@@ -0,0 +1,403 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Nikolai Vladimirov (nvladimirov.work@gmail.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_RESP3_FLAT_TREE_HPP
|
||||
#define BOOST_REDIS_RESP3_FLAT_TREE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
|
||||
#include <boost/core/span.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
namespace adapter::detail {
|
||||
template <class> class general_aggregate;
|
||||
} // namespace adapter::detail
|
||||
|
||||
namespace resp3 {
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct flat_buffer {
|
||||
std::unique_ptr<char[]> data;
|
||||
std::size_t size = 0u;
|
||||
std::size_t capacity = 0u;
|
||||
std::size_t reallocs = 0u;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** @brief A generic response that stores data contiguously.
|
||||
*
|
||||
* Implements a container of RESP3 nodes. It's similar to @ref boost::redis::resp3::tree,
|
||||
* but node data is stored contiguously. This allows for amortized no allocations
|
||||
* when re-using `flat_tree` objects. Like `tree`, it can contain the response
|
||||
* to several Redis commands or several server pushes. Use @ref get_total_msgs
|
||||
* to obtain how many responses this object contains.
|
||||
*
|
||||
* Objects are typically created by the user and passed to @ref connection::async_exec
|
||||
* to be used as response containers. Once populated, they can be used as a const range
|
||||
* of @ref resp3::node_view objects. The usual random access range methods (like @ref at, @ref size or
|
||||
* @ref front) are provided. Once populated, `flat_tree` can't be modified,
|
||||
* except for @ref clear and assignment.
|
||||
*
|
||||
* `flat_tree` models `std::ranges::contiguous_range`.
|
||||
*
|
||||
* A `flat_tree` is conceptually similar to a pair of `std::vector` objects, one holding
|
||||
* @ref resp3::node_view objects, and another owning the the string data that these views
|
||||
* point to. The node capacity and the data capacity are the capacities of these two vectors.
|
||||
*/
|
||||
class flat_tree {
|
||||
public:
|
||||
/**
|
||||
* @brief The type of the iterators returned by @ref begin and @ref end.
|
||||
*
|
||||
* It is guaranteed to be a contiguous iterator. While this is currently a pointer,
|
||||
* users shouldn't rely on this fact, as the exact implementation may change between releases.
|
||||
*/
|
||||
using iterator = const node_view*;
|
||||
|
||||
/**
|
||||
* @brief The type of the iterators returned by @ref rbegin and @ref rend.
|
||||
*
|
||||
* As with @ref iterator, users should treat this type as an unspecified
|
||||
* contiguous iterator type rather than assuming a specific type.
|
||||
*/
|
||||
using reverse_iterator = std::reverse_iterator<iterator>;
|
||||
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*
|
||||
* Constructs an empty tree, with no nodes, zero node capacity and zero data capacity.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
flat_tree() = default;
|
||||
|
||||
/**
|
||||
* @brief Move constructor.
|
||||
*
|
||||
* Constructs a tree by taking ownership of the nodes in `other`.
|
||||
*
|
||||
* @par Object lifetimes
|
||||
* Iterators, pointers and references to the nodes and strings in `other` remain valid.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
flat_tree(flat_tree&& other) noexcept = default;
|
||||
|
||||
/**
|
||||
* @brief Copy constructor.
|
||||
*
|
||||
* Constructs a tree by copying the nodes in `other`. After the copy,
|
||||
* `*this` and `other` have independent lifetimes (usual copy semantics).
|
||||
*
|
||||
* @par Exception safety
|
||||
* Strong guarantee. Memory allocations might throw.
|
||||
*/
|
||||
flat_tree(flat_tree const& other);
|
||||
|
||||
/**
|
||||
* @brief Move assignment.
|
||||
*
|
||||
* Replaces the nodes in `*this` by taking ownership of the nodes in `other`.
|
||||
* `other` is left in a valid but unspecified state.
|
||||
*
|
||||
* @par Object lifetimes
|
||||
* Iterators, pointers and references to the nodes and strings in `other` remain valid.
|
||||
* Iterators, pointers and references to the nodes and strings in `*this` are invalidated.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
flat_tree& operator=(flat_tree&& other) = default;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment.
|
||||
*
|
||||
* Replaces the nodes in `*this` by copying the nodes in `other`.
|
||||
* After the copy, `*this` and `other` have independent lifetimes (usual copy semantics).
|
||||
*
|
||||
* @par Object lifetimes
|
||||
* Iterators, pointers and references to the nodes and strings in `*this` are invalidated.
|
||||
*
|
||||
* @par Exception safety
|
||||
* Basic guarantee. Memory allocations might throw.
|
||||
*/
|
||||
flat_tree& operator=(const flat_tree& other);
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the node range.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator to the first node.
|
||||
*/
|
||||
iterator begin() const noexcept { return data(); }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator past the last element in the node range.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator past the last element in the node range.
|
||||
*/
|
||||
iterator end() const noexcept { return data() + size(); }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the reversed node range.
|
||||
*
|
||||
* Allows iterating the range of nodes in reverse order.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator to the first node of the reversed range.
|
||||
*/
|
||||
reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator past the last element of the reversed node range.
|
||||
*
|
||||
* Allows iterating the range of nodes in reverse order.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator past the last element of the reversed node range.
|
||||
*/
|
||||
reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the node at the specified position (checked access).
|
||||
*
|
||||
* @par Exception safety
|
||||
* Strong guarantee. Throws `std::out_of_range` if `i >= size()`.
|
||||
*
|
||||
* @param i Position of the node to return.
|
||||
* @returns A reference to the node at position `i`.
|
||||
*/
|
||||
const node_view& at(std::size_t i) const;
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the node at the specified position (unchecked access).
|
||||
*
|
||||
* @par Precondition
|
||||
* `i < size()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @param i Position of the node to return.
|
||||
* @returns A reference to the node at position `i`.
|
||||
*/
|
||||
const node_view& operator[](std::size_t i) const noexcept { return get_view()[i]; }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the first node.
|
||||
*
|
||||
* @par Precondition
|
||||
* `!empty()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A reference to the first node.
|
||||
*/
|
||||
const node_view& front() const noexcept { return get_view().front(); }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the last node.
|
||||
*
|
||||
* @par Precondition
|
||||
* `!empty()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A reference to the last node.
|
||||
*/
|
||||
const node_view& back() const noexcept { return get_view().back(); }
|
||||
|
||||
/**
|
||||
* @brief Returns a pointer to the underlying node storage.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A pointer to the underlying node array.
|
||||
*/
|
||||
const node_view* data() const noexcept { return view_tree_.data(); }
|
||||
|
||||
/**
|
||||
* @brief Checks whether the tree is empty.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns `true` if the tree contains no nodes, `false` otherwise.
|
||||
*/
|
||||
bool empty() const noexcept { return size() == 0u; }
|
||||
|
||||
/**
|
||||
* @brief Returns the number of nodes in the tree.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The number of nodes.
|
||||
*/
|
||||
std::size_t size() const noexcept { return node_tmp_offset_; }
|
||||
|
||||
/** @brief Reserves capacity for incoming data.
|
||||
*
|
||||
* Adding nodes (e.g. by passing the tree to `async_exec`)
|
||||
* won't cause reallocations until the data or node capacities
|
||||
* are exceeded, following the usual vector semantics.
|
||||
* The implementation might reserve more capacity than the one requested.
|
||||
*
|
||||
* @par Object lifetimes
|
||||
* References to the nodes and strings in `*this` are invalidated.
|
||||
*
|
||||
* @par Exception safety
|
||||
* Basic guarantee. Memory allocations might throw.
|
||||
*
|
||||
* @param bytes Number of bytes to reserve for data.
|
||||
* @param nodes Number of nodes to reserve.
|
||||
*/
|
||||
void reserve(std::size_t bytes, std::size_t nodes);
|
||||
|
||||
/** @brief Clears the tree so it contains no nodes.
|
||||
*
|
||||
* Calling this function removes every node, making
|
||||
* the range contain no nodes, and @ref get_total_msgs
|
||||
* return zero. It does not modify the object's capacity.
|
||||
*
|
||||
* To re-use a `flat_tree` for several requests,
|
||||
* use `clear()` before each `async_exec` call.
|
||||
*
|
||||
* @par Object lifetimes
|
||||
* References to the nodes and strings in `*this` are invalidated.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
void clear() noexcept;
|
||||
|
||||
/** @brief Returns the size of the data buffer, in bytes.
|
||||
*
|
||||
* You may use this function to calculate how much capacity
|
||||
* should be reserved for data when calling @ref reserve.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The number of bytes in use in the data buffer.
|
||||
*/
|
||||
auto data_size() const noexcept -> std::size_t { return data_tmp_offset_; }
|
||||
|
||||
/** @brief Returns the capacity of the node container.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The capacity of the object, in number of nodes.
|
||||
*/
|
||||
auto capacity() const noexcept -> std::size_t { return view_tree_.capacity(); }
|
||||
|
||||
/** @brief Returns the capacity of the data buffer, in bytes.
|
||||
*
|
||||
* Note that the actual capacity of the data buffer may be bigger
|
||||
* than the one requested by @ref reserve.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The capacity of the data buffer, in bytes.
|
||||
*/
|
||||
auto data_capacity() const noexcept -> std::size_t { return data_.capacity; }
|
||||
|
||||
/** @brief Returns the number of memory reallocations that took place in the data buffer.
|
||||
*
|
||||
* This function returns how many reallocations in the data buffer were performed and
|
||||
* can be useful to determine how much memory to reserve upfront.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The number of times that the data buffer reallocated its memory.
|
||||
*/
|
||||
auto get_reallocs() const noexcept -> std::size_t { return data_.reallocs; }
|
||||
|
||||
/** @brief Returns the number of complete RESP3 messages contained in this object.
|
||||
*
|
||||
* This value is equal to the number of nodes in the tree with a depth of zero.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The number of complete RESP3 messages contained in this object.
|
||||
*/
|
||||
std::size_t get_total_msgs() const noexcept { return total_msgs_; }
|
||||
|
||||
private:
|
||||
template <class> friend class adapter::detail::general_aggregate;
|
||||
|
||||
span<const node_view> get_view() const noexcept { return {data(), size()}; }
|
||||
void notify_init();
|
||||
void notify_done();
|
||||
|
||||
// Push a new node to the response
|
||||
void push(node_view const& node);
|
||||
|
||||
detail::flat_buffer data_;
|
||||
view_tree view_tree_;
|
||||
std::size_t total_msgs_ = 0u;
|
||||
|
||||
// flat_tree supports a "temporary working area" for incrementally reading messages.
|
||||
// Nodes in the tmp area are not part of the object representation until they
|
||||
// are committed with notify_done().
|
||||
// These offsets delimit this area.
|
||||
std::size_t node_tmp_offset_ = 0u;
|
||||
std::size_t data_tmp_offset_ = 0u;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Equality operator.
|
||||
* @relates flat_tree
|
||||
*
|
||||
* Two trees are equal if they contain the same nodes in the same order.
|
||||
* Capacities are not taken into account.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
bool operator==(flat_tree const&, flat_tree const&);
|
||||
|
||||
/**
|
||||
* @brief Inequality operator.
|
||||
* @relates flat_tree
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
inline bool operator!=(flat_tree const& lhs, flat_tree const& rhs) { return !(lhs == rhs); }
|
||||
|
||||
} // namespace resp3
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_RESP3_FLAT_TREE_HPP
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
auto to_string(type t) noexcept -> char const*
|
||||
|
||||
@@ -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)
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
/** @brief A node in the response tree.
|
||||
@@ -43,7 +47,7 @@ struct basic_node {
|
||||
* @param b Right hand side node object.
|
||||
*/
|
||||
template <class String>
|
||||
auto operator==(basic_node<String> const& a, basic_node<String> const& b)
|
||||
bool operator==(basic_node<String> const& a, basic_node<String> const& b)
|
||||
{
|
||||
// clang-format off
|
||||
return a.aggregate_size == b.aggregate_size
|
||||
@@ -53,6 +57,18 @@ auto operator==(basic_node<String> const& a, basic_node<String> const& b)
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
/** @brief Inequality operator for RESP3 nodes.
|
||||
* @relates basic_node
|
||||
*
|
||||
* @param a Left hand side node object.
|
||||
* @param b Right hand side node object.
|
||||
*/
|
||||
template <class String>
|
||||
bool operator!=(basic_node<String> const& a, basic_node<String> const& b)
|
||||
{
|
||||
return !(a == b);
|
||||
};
|
||||
|
||||
/// A node in the response tree that owns its data.
|
||||
using node = basic_node<std::string>;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
// NOTE: Consider detecting tuples in the type in the parameter pack
|
||||
// to calculate the header size correctly.
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
/** @brief Adds a bulk to the request.
|
||||
@@ -35,6 +32,10 @@ namespace boost::redis::resp3 {
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* The function must add exactly one bulk string RESP3 node.
|
||||
* If you're using `boost_redis_to_bulk` with a string argument,
|
||||
* you're safe.
|
||||
*
|
||||
* @param payload Storage on which data will be copied into.
|
||||
* @param data Data that will be serialized and stored in `payload`.
|
||||
*/
|
||||
@@ -100,6 +101,11 @@ struct bulk_counter<std::pair<T, U>> {
|
||||
static constexpr auto size = 2U;
|
||||
};
|
||||
|
||||
template <class... T>
|
||||
struct bulk_counter<std::tuple<T...>> {
|
||||
static constexpr auto size = sizeof...(T);
|
||||
};
|
||||
|
||||
void add_blob(std::string& payload, std::string_view blob);
|
||||
void add_separator(std::string& payload);
|
||||
|
||||
|
||||
29
include/boost/redis/resp3/tree.hpp
Normal file
29
include/boost/redis/resp3/tree.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
/* 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_RESP3_TREE_HPP
|
||||
#define BOOST_REDIS_RESP3_TREE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
/// A RESP3 tree that owns its data.
|
||||
template <class String, class Allocator = std::allocator<basic_node<String>>>
|
||||
using basic_tree = std::vector<basic_node<String>, Allocator>;
|
||||
|
||||
/// A RESP3 tree that owns its data.
|
||||
using tree = basic_tree<std::string>;
|
||||
|
||||
/// A RESP3 tree whose data are `std::string_views`.
|
||||
using view_tree = basic_tree<std::string_view>;
|
||||
|
||||
}
|
||||
|
||||
#endif // BOOST_REDIS_RESP3_RESPONSE_HPP
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <iosfwd>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
#define BOOST_REDIS_RESPONSE_HPP
|
||||
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
@@ -29,9 +29,12 @@ using response = std::tuple<adapter::result<Ts>...>;
|
||||
* [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR)
|
||||
* view of the response tree.
|
||||
*/
|
||||
using generic_response = adapter::result<std::vector<resp3::node>>;
|
||||
using generic_response = adapter::result<resp3::tree>;
|
||||
|
||||
/** @brief Consume on response from a generic response
|
||||
/// Similar to @ref boost::redis::generic_response but stores data contiguously.
|
||||
using generic_flat_response = adapter::result<resp3::flat_tree>;
|
||||
|
||||
/** @brief (Deprecated) Consume on response from a generic response
|
||||
*
|
||||
* This function rotates the elements so that the start of the next
|
||||
* response becomes the new front element. For example the output of
|
||||
@@ -70,13 +73,15 @@ using generic_response = adapter::result<std::vector<resp3::node>>;
|
||||
* @param r The response to modify.
|
||||
* @param ec Will be populated in case of error.
|
||||
*/
|
||||
BOOST_DEPRECATED("This function is not needed anymore to consume server pushes.")
|
||||
void consume_one(generic_response& r, system::error_code& ec);
|
||||
|
||||
/**
|
||||
* @brief Throwing overload of `consume_one`.
|
||||
* @brief (Deprecated) Throwing overload of `consume_one`.
|
||||
*
|
||||
* @param r The response to modify.
|
||||
*/
|
||||
BOOST_DEPRECATED("This function is not needed anymore to consume server pushes.")
|
||||
void consume_one(generic_response& r);
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
@@ -4,18 +4,24 @@
|
||||
* 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/exec_one_fsm.ipp>
|
||||
#include <boost/redis/impl/flat_tree.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/receive_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/sentinel_resolve_fsm.ipp>
|
||||
#include <boost/redis/impl/subscription_tracker.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)
|
||||
|
||||
@@ -35,31 +35,51 @@ endmacro()
|
||||
# Unit tests
|
||||
make_test(test_low_level)
|
||||
make_test(test_request)
|
||||
make_test(test_serialization)
|
||||
make_test(test_low_level_sync_sans_io)
|
||||
make_test(test_any_adapter)
|
||||
make_test(test_exec_fsm)
|
||||
make_test(test_log_to_file)
|
||||
make_test(test_conn_logging)
|
||||
make_test(test_exec_fsm)
|
||||
make_test(test_exec_one_fsm)
|
||||
make_test(test_writer_fsm)
|
||||
make_test(test_reader_fsm)
|
||||
make_test(test_connect_fsm)
|
||||
make_test(test_sentinel_resolve_fsm)
|
||||
make_test(test_receive_fsm)
|
||||
make_test(test_run_fsm)
|
||||
make_test(test_compose_setup_request)
|
||||
make_test(test_setup_adapter)
|
||||
make_test(test_multiplexer)
|
||||
make_test(test_parse_sentinel_response)
|
||||
make_test(test_update_sentinel_list)
|
||||
make_test(test_flat_tree)
|
||||
make_test(test_generic_flat_response)
|
||||
make_test(test_read_buffer)
|
||||
make_test(test_subscription_tracker)
|
||||
|
||||
# Tests that require a real Redis server
|
||||
make_test(test_conn_quit)
|
||||
make_test(test_conn_exec_retry)
|
||||
make_test(test_conn_exec_error)
|
||||
make_test(test_run)
|
||||
make_test(test_conn_run_cancel)
|
||||
make_test(test_conn_check_health)
|
||||
make_test(test_conn_exec)
|
||||
make_test(test_conn_push)
|
||||
make_test(test_conn_push2)
|
||||
make_test(test_conn_monitor)
|
||||
make_test(test_conn_reconnect)
|
||||
make_test(test_conn_exec_cancel)
|
||||
make_test(test_conn_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)
|
||||
make_test(test_conn_sentinel)
|
||||
|
||||
# Coverage
|
||||
set(
|
||||
|
||||
19
test/Jamfile
19
test/Jamfile
@@ -42,6 +42,7 @@ lib redis_test_common
|
||||
:
|
||||
boost_redis.cpp
|
||||
common.cpp
|
||||
sansio_utils.cpp
|
||||
: requirements $(requirements)
|
||||
: usage-requirements $(requirements)
|
||||
;
|
||||
@@ -51,12 +52,28 @@ lib redis_test_common
|
||||
local tests =
|
||||
test_low_level
|
||||
test_request
|
||||
test_serialization
|
||||
test_low_level_sync_sans_io
|
||||
test_any_adapter
|
||||
test_exec_fsm
|
||||
test_log_to_file
|
||||
test_conn_logging
|
||||
test_exec_fsm
|
||||
test_exec_one_fsm
|
||||
test_writer_fsm
|
||||
test_reader_fsm
|
||||
test_sentinel_resolve_fsm
|
||||
test_receive_fsm
|
||||
test_run_fsm
|
||||
test_connect_fsm
|
||||
test_compose_setup_request
|
||||
test_setup_adapter
|
||||
test_multiplexer
|
||||
test_parse_sentinel_response
|
||||
test_update_sentinel_list
|
||||
test_flat_tree
|
||||
test_generic_flat_response
|
||||
test_read_buffer
|
||||
test_subscription_tracker
|
||||
;
|
||||
|
||||
# Build and run the tests
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
struct run_callback {
|
||||
std::shared_ptr<boost::redis::connection> conn;
|
||||
@@ -50,6 +57,7 @@ boost::redis::config make_test_config()
|
||||
{
|
||||
boost::redis::config cfg;
|
||||
cfg.addr.host = get_server_hostname();
|
||||
cfg.reconnect_wait_interval = 50ms; // make tests involving reconnection faster
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -69,9 +77,59 @@ void run_coroutine_test(net::awaitable<void> op, std::chrono::steady_clock::dura
|
||||
}
|
||||
#endif // BOOST_ASIO_HAS_CO_AWAIT
|
||||
|
||||
void append_read_data(boost::redis::detail::read_buffer& rbuf, std::string_view data)
|
||||
// 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)
|
||||
{
|
||||
auto const buffer = rbuf.get_append_buffer();
|
||||
BOOST_ASSERT(data.size() <= buffer.size());
|
||||
std::copy(data.begin(), data.end(), buffer.begin());
|
||||
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);
|
||||
}
|
||||
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password)
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
boost::redis::connection conn{ioc};
|
||||
|
||||
boost::redis::config cfg;
|
||||
cfg.addr.port = port;
|
||||
|
||||
// Enable the user and grant them permissions on everything
|
||||
boost::redis::request req;
|
||||
req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all");
|
||||
|
||||
bool run_finished = false, exec_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](boost::system::error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, boost::system::error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
}
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to)
|
||||
{
|
||||
return {
|
||||
boost::redis::logger::level::info,
|
||||
[&to](boost::redis::logger::level, std::string_view msg) {
|
||||
to += msg;
|
||||
to += '\n';
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/detail/reader_fsm.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
@@ -11,6 +12,8 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#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
|
||||
@@ -36,4 +39,11 @@ void run(
|
||||
boost::system::error_code ec = boost::asio::error::operation_aborted,
|
||||
boost::redis::operation op = boost::redis::operation::receive);
|
||||
|
||||
void append_read_data(boost::redis::detail::read_buffer& rbuf, std::string_view data);
|
||||
// Finds a value in the output of the CLIENT INFO command
|
||||
// format: key1=value1 key2=value2
|
||||
std::string_view find_client_info(std::string_view client_info, std::string_view key);
|
||||
|
||||
// Connects to the Redis server at the given port and creates a user
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password);
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to);
|
||||
|
||||
28
test/print_node.hpp
Normal file
28
test/print_node.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
/* 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_PRINT_NODE_HPP
|
||||
#define BOOST_REDIS_TEST_PRINT_NODE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
template <class String>
|
||||
std::ostream& operator<<(std::ostream& os, basic_node<String> const& nd)
|
||||
{
|
||||
return os << "node{ .data_type=" << to_string(nd.data_type)
|
||||
<< ", .aggregate_size=" << nd.aggregate_size << ", .depth=" << nd.depth
|
||||
<< ", .value=" << nd.value << "}";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
97
test/sansio_utils.cpp
Normal file
97
test/sansio_utils.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 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/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
|
||||
#include <boost/assert/source_location.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)});
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc)
|
||||
{
|
||||
std::vector<resp3::node> nodes;
|
||||
any_adapter adapter{nodes};
|
||||
|
||||
for (std::string_view resp : msgs) {
|
||||
resp3::parser p;
|
||||
system::error_code ec;
|
||||
bool done = resp3::parse(p, resp, adapter, ec);
|
||||
if (!BOOST_TEST(done))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_EQ(ec, system::error_code()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
64
test/sansio_utils.hpp
Normal file
64
test/sansio_utils.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
/* 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/redis/resp3/node.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Creates a vector of nodes from a set of RESP3 messages.
|
||||
// Using the raw RESP values ensures that the correct
|
||||
// node tree is built, which is not always obvious
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc = BOOST_CURRENT_LOCATION);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::resp3::flat_tree;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::any_adapter;
|
||||
@@ -24,10 +25,12 @@ BOOST_AUTO_TEST_CASE(any_adapter_response_types)
|
||||
response<int> r1;
|
||||
response<int, std::string> r2;
|
||||
generic_response r3;
|
||||
flat_tree r4;
|
||||
|
||||
BOOST_CHECK_NO_THROW(any_adapter{r1});
|
||||
BOOST_CHECK_NO_THROW(any_adapter{r2});
|
||||
BOOST_CHECK_NO_THROW(any_adapter{r3});
|
||||
BOOST_CHECK_NO_THROW(any_adapter{r4});
|
||||
BOOST_CHECK_NO_THROW(any_adapter{ignore});
|
||||
}
|
||||
|
||||
|
||||
256
test/test_compose_setup_request.cpp
Normal file
256
test/test_compose_setup_request.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
//
|
||||
// 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/subscription_tracker.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/asio/error.hpp>
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::compose_setup_request;
|
||||
using detail::subscription_tracker;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture {
|
||||
subscription_tracker tracker;
|
||||
request out;
|
||||
config cfg;
|
||||
|
||||
void run(std::string_view expected_payload, boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
out.push("PING", "leftover"); // verify that we clear the request
|
||||
|
||||
compose_setup_request(cfg, tracker, out);
|
||||
|
||||
if (!BOOST_TEST_EQ(out.payload(), expected_payload))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.has_hello_priority()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.get_config().cancel_if_unresponded))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.get_config().cancel_on_connection_lost))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
void test_hello()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
|
||||
fix.run("*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n");
|
||||
}
|
||||
|
||||
void test_select()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.database_index = 10;
|
||||
|
||||
fix.run(
|
||||
"*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");
|
||||
}
|
||||
|
||||
void test_clientname()
|
||||
{
|
||||
fixture fix;
|
||||
|
||||
fix.run("*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n");
|
||||
}
|
||||
|
||||
void test_auth()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run("*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");
|
||||
}
|
||||
|
||||
void test_auth_empty_password()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
|
||||
fix.run("*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");
|
||||
}
|
||||
|
||||
void test_auth_setname()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "mytest";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run(
|
||||
"*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");
|
||||
}
|
||||
|
||||
void test_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "mytest";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
fix.cfg.database_index = 4;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
|
||||
fix.run(
|
||||
"*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");
|
||||
}
|
||||
|
||||
// Regression check: we set the priority flag
|
||||
void test_use_setup_no_hello()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
|
||||
fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n");
|
||||
}
|
||||
|
||||
// Regression check: we set the relevant cancellation flags in the request
|
||||
void test_use_setup_flags()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
fix.cfg.setup.get_config().cancel_if_unresponded = false;
|
||||
fix.cfg.setup.get_config().cancel_on_connection_lost = false;
|
||||
|
||||
fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n");
|
||||
}
|
||||
|
||||
// If we have tracked subscriptions, these are added at the end
|
||||
void test_tracked_subscriptions()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
void test_tracked_subscriptions_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("PING", "value");
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$4\r\nPING\r\n$5\r\nvalue\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
// When using Sentinel, a ROLE command is added. This works
|
||||
// both with the old HELLO and new setup strategies, and with tracked subscriptions
|
||||
void test_sentinel_auth()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run(
|
||||
"*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"
|
||||
"*1\r\n$4\r\nROLE\r\n");
|
||||
}
|
||||
|
||||
void test_sentinel_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.push("SELECT", 42);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*2\r\n$6\r\nSELECT\r\n$2\r\n42\r\n"
|
||||
"*1\r\n$4\r\nROLE\r\n");
|
||||
}
|
||||
|
||||
void test_sentinel_tracked_subscriptions()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*1\r\n$4\r\nROLE\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_hello();
|
||||
test_select();
|
||||
test_clientname();
|
||||
test_auth();
|
||||
test_auth_empty_password();
|
||||
test_auth_setname();
|
||||
test_use_setup();
|
||||
test_use_setup_no_hello();
|
||||
test_use_setup_flags();
|
||||
test_tracked_subscriptions();
|
||||
test_tracked_subscriptions_use_setup();
|
||||
test_sentinel_auth();
|
||||
test_sentinel_use_setup();
|
||||
test_sentinel_tracked_subscriptions();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
134
test/test_conn_cancel_after.cpp
Normal file
134
test/test_conn_cancel_after.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
template <class Connection>
|
||||
void test_receive2()
|
||||
{
|
||||
// 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_receive2(asio::cancel_after(1ms, [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
|
||||
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>();
|
||||
|
||||
test_receive2<basic_connection<asio::io_context::executor_type>>();
|
||||
test_receive2<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,239 @@ 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;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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)
|
||||
@@ -27,6 +27,7 @@ using error_code = boost::system::error_code;
|
||||
using boost::redis::operation;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::resp3::flat_tree;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::logger;
|
||||
@@ -43,8 +44,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;
|
||||
}
|
||||
@@ -53,39 +55,40 @@ std::ostream& operator<<(std::ostream& os, usage const& u)
|
||||
|
||||
namespace {
|
||||
|
||||
auto push_consumer(connection& conn, int expected) -> net::awaitable<void>
|
||||
auto receiver(connection& conn, flat_tree& resp, std::size_t expected) -> net::awaitable<void>
|
||||
{
|
||||
int c = 0;
|
||||
for (error_code ec;;) {
|
||||
conn.receive(ec);
|
||||
if (ec == error::sync_receive_push_failed) {
|
||||
ec = {};
|
||||
co_await conn.async_receive(net::redirect_error(ec));
|
||||
} else if (!ec) {
|
||||
//std::cout << "Skipping suspension." << std::endl;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
BOOST_TEST(false, "push_consumer error: " << ec.message());
|
||||
co_return;
|
||||
}
|
||||
if (++c == expected)
|
||||
break;
|
||||
std::size_t push_counter = 0;
|
||||
while (push_counter != expected) {
|
||||
co_await conn.async_receive2();
|
||||
push_counter += resp.get_total_msgs();
|
||||
resp.clear();
|
||||
}
|
||||
|
||||
conn.cancel();
|
||||
}
|
||||
|
||||
auto echo_session(connection& conn, const request& pubs, int n) -> net::awaitable<void>
|
||||
auto echo_session(connection& conn, const request& req, std::size_t n) -> net::awaitable<void>
|
||||
{
|
||||
for (auto i = 0; i < n; ++i)
|
||||
co_await conn.async_exec(pubs);
|
||||
for (auto i = 0u; i < n; ++i)
|
||||
co_await conn.async_exec(req);
|
||||
}
|
||||
|
||||
void rethrow_on_error(std::exception_ptr exc)
|
||||
{
|
||||
if (exc)
|
||||
if (exc) {
|
||||
BOOST_TEST(false);
|
||||
std::rethrow_exception(exc);
|
||||
}
|
||||
}
|
||||
|
||||
request make_pub_req(std::size_t n_pubs)
|
||||
{
|
||||
request req;
|
||||
req.push("PING");
|
||||
for (std::size_t i = 0u; i < n_pubs; ++i)
|
||||
req.push("PUBLISH", "channel", "payload");
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(echo_stress)
|
||||
@@ -94,26 +97,25 @@ 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.
|
||||
constexpr int sessions = 150;
|
||||
constexpr std::size_t sessions = 150u;
|
||||
|
||||
// The number of pings that will be sent by each session.
|
||||
constexpr int msgs = 200;
|
||||
constexpr std::size_t msgs = 200u;
|
||||
|
||||
// The number of publishes that will be sent by each session with
|
||||
// each message.
|
||||
constexpr int n_pubs = 25;
|
||||
constexpr std::size_t n_pubs = 25u;
|
||||
|
||||
// This is the total number of pushes we will receive.
|
||||
constexpr int total_pushes = sessions * msgs * n_pubs + 1;
|
||||
constexpr std::size_t total_pushes = sessions * msgs * n_pubs + 1;
|
||||
|
||||
request pubs;
|
||||
pubs.push("PING");
|
||||
for (int i = 0; i < n_pubs; ++i)
|
||||
pubs.push("PUBLISH", "channel", "payload");
|
||||
flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
request const pub_req = make_pub_req(n_pubs);
|
||||
|
||||
// Run the connection
|
||||
bool run_finished = false, subscribe_finished = false;
|
||||
@@ -123,19 +125,19 @@ BOOST_AUTO_TEST_CASE(echo_stress)
|
||||
std::clog << "async_run finished" << std::endl;
|
||||
});
|
||||
|
||||
// Op that will consume the pushes counting down until all expected
|
||||
// pushes have been received.
|
||||
net::co_spawn(ctx, receiver(conn, resp, total_pushes), rethrow_on_error);
|
||||
|
||||
// Subscribe, then launch the coroutines
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.subscribe({"channel"});
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
subscribe_finished = true;
|
||||
BOOST_TEST(ec == error_code());
|
||||
|
||||
// Op that will consume the pushes counting down until all expected
|
||||
// pushes have been received.
|
||||
net::co_spawn(ctx, push_consumer(conn, total_pushes), rethrow_on_error);
|
||||
|
||||
for (int i = 0; i < sessions; ++i)
|
||||
net::co_spawn(ctx, echo_session(conn, pubs, msgs), rethrow_on_error);
|
||||
for (std::size_t i = 0; i < sessions; ++i)
|
||||
net::co_spawn(ctx, echo_session(conn, pub_req, msgs), rethrow_on_error);
|
||||
});
|
||||
|
||||
// Run the test
|
||||
@@ -144,7 +146,11 @@ BOOST_AUTO_TEST_CASE(echo_stress)
|
||||
BOOST_TEST(subscribe_finished);
|
||||
|
||||
// Print statistics
|
||||
std::cout << "-------------------\n" << conn.get_usage() << std::endl;
|
||||
std::cout << "-------------------\n"
|
||||
<< "Usage data: \n"
|
||||
<< conn.get_usage() << "\n"
|
||||
<< "-------------------\n"
|
||||
<< "Reallocations: " << resp.get_reallocs() << std::endl;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
@@ -122,68 +123,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
|
||||
@@ -196,8 +135,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;
|
||||
@@ -243,4 +181,31 @@ BOOST_AUTO_TEST_CASE(exec_any_adapter)
|
||||
BOOST_TEST(std::get<0>(res).value() == "PONG");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(exec_generic_flat_response)
|
||||
{
|
||||
// Executing with a generic_flat_response works
|
||||
request req;
|
||||
req.push("PING", "PONG");
|
||||
boost::redis::generic_flat_response resp;
|
||||
|
||||
net::io_context ioc;
|
||||
|
||||
auto conn = std::make_shared<connection>(ioc);
|
||||
|
||||
bool finished = false;
|
||||
|
||||
conn->async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST(ec == error_code());
|
||||
conn->cancel();
|
||||
finished = true;
|
||||
});
|
||||
|
||||
run(conn);
|
||||
ioc.run_for(test_timeout);
|
||||
BOOST_TEST_REQUIRE(finished);
|
||||
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST(resp.value().front().value == "PONG");
|
||||
}
|
||||
|
||||
} // 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
|
||||
@@ -269,9 +269,9 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
|
||||
generic_response gresp;
|
||||
conn->set_receive_response(gresp);
|
||||
|
||||
auto c3 = [&](error_code ec, std::size_t) {
|
||||
auto c3 = [&](error_code ec) {
|
||||
c3_called = true;
|
||||
std::cout << "async_receive" << std::endl;
|
||||
std::cout << "async_receive2" << std::endl;
|
||||
BOOST_TEST(!ec);
|
||||
BOOST_TEST(gresp.has_error());
|
||||
BOOST_CHECK_EQUAL(gresp.error().data_type, resp3::type::simple_error);
|
||||
@@ -281,7 +281,7 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
|
||||
conn->cancel(operation::reconnection);
|
||||
};
|
||||
|
||||
conn->async_receive(c3);
|
||||
conn->async_receive2(c3);
|
||||
|
||||
run(conn);
|
||||
|
||||
@@ -292,4 +292,38 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
|
||||
BOOST_TEST(c3_called);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user