2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-20 05:02:12 +00:00

Compare commits

..

65 Commits

Author SHA1 Message Date
Marcelo Zimbres
98f5a55c4a Laze rotations 2025-12-29 02:07:36 +01:00
Anarthal (Rubén Pérez)
7750a6b126 Splits read_buffer tests to a separate file (#368) 2025-12-03 08:53:03 +01:00
Anarthal (Rubén Pérez)
2bbf0090b5 Mark consume_one as deprecated (#365)
close #353
2025-12-02 11:20:56 +01:00
Anarthal (Rubén Pérez)
02632b31c6 Removes ostream inclusion from public headers (#364)
close #361
2025-12-02 11:20:32 +01:00
Anarthal (Rubén Pérez)
6005ebd04a Fixes std::tuple serialization and adds tests (#363)
* Fixes a problem that caused passing ranges containing tuples into `request::push_range` to generate invalid commands.
* Adds test_serialization
* Updates request reference docs to reflect the requirements of the types passed to push and push_range

close #360
2025-12-02 11:20:01 +01:00
Anarthal (Rubén Pérez)
755d14a10d Renames test_setup_request_utils to test_compose_setup_request (#359) 2025-11-29 22:26:37 +01:00
Anarthal (Rubén Pérez)
d9e4b2c720 Improves flat_tree implementation (#358)
* Makes flat_tree implementation use a custom buffer. This allows:
  * Never dangling nodes (previously, node values could dangle after calling reserve() or if notify_done() wasn't called).
  * Reduced memory consumption
  * Increased runtime speed
* Changes flat_tree assignment to the usual signature and semantics
* Fixes a bug causing an assertion to trigger when copy-constructing an empty flat_tree.
* Changes basic_node operator== and operator!= return type 
* Adds generic_flat_response, basic_tree, tree, view_tree, flat_tree to the reference page.
* Adds a missing resp3:: qualifier to all names in the reference page that belong to the resp3 namespace.
* Adds reference documentation to flat_tree.
* Mentions generic_flat_response in the discussion.
* Adds operator!= for basic_node to basic_node's reference page.
* Adds test_flat_tree.

close #357 
close #354 
close #352
2025-11-29 21:35:53 +01:00
Marcelo
91afb4a279 Fixes SBO problem and adds some tests (#356) 2025-11-20 14:38:49 +01:00
Anarthal (Rubén Pérez)
bdd9c327c1 Adds Sentinel support (#345)
close #237
close #269
close #268
close #229
2025-11-19 22:31:19 +01:00
Marcelo
00f3ec9b78 Merge pull request #340 from boostorg/263-marcelo
Concludes the work on the generic_flat_response started by Nikolai Vladimirov
2025-11-18 14:10:23 +01:00
Marcelo Zimbres
b365b96228 Fixup Adds generic_flat_response typedef 2025-11-10 23:21:16 +01:00
Marcelo Zimbres
2c1f1c4c50 Concludes the work started by Nikolai Vladimirov on the generic_flat_response 2025-11-09 21:37:39 +01:00
Nikolai Vladimirov
6ff474008f Cosmetic changes 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
bd799aff96 create flat_response_value::add_node() 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
c284960549 Replaced generic_response with generic_flat_response in tests 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
b1420d3d1d Adjust code base to new changes 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
84fa39918f Added API for generic_flat_response, replaced generic_response with new response type in tests 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
019a080b65 Get rid of type erasion in details::prepare_done 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
53e5ae0cd4 Avoid turning throwing consume_one into template 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
1d9f9ab0e3 Moved implementation of push_back to general_aggregate 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
5444e077f9 Defined done_fn_type in adapters.hpp 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
ecd1573257 Call prepare_done function to form response 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
9419c857fd Corrected and cleaner implementation with comments 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
b78cf818e0 reserve function 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
7d959c1039 Addressed some comments 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
ccb17f89cd Draft version without consume_one implementation 2025-11-08 21:37:40 +01:00
Anarthal (Rubén Pérez)
c9375a44eb Adds release notes for Boost 1.89 and Boost 1.90 (#347) 2025-11-03 14:53:32 +01:00
Anarthal (Rubén Pérez)
c88f9f4666 Fixes a potential use-after-move bug in async_connect (#343) 2025-10-30 14:49:57 +01:00
Anarthal (Rubén Pérez)
682bec618d Adds request::append() (#342)
close #341
2025-10-27 15:18:46 +01:00
Anarthal (Rubén Pérez)
6791e759f9 Adds support for PUNSUBSCRIBE (#339)
Adds a test covering UNSUBSCRIBE and PUNSUBSCRIBE

close #306
2025-10-25 00:41:49 +02:00
Anarthal (Rubén Pérez)
0460b38e14 Simplifies setup_request_utils.hpp (#338)
Removes unused clear_response
Removes include in connection.hpp
Moves the header to impl/ and makes functions inline
2025-10-23 22:37:50 +02:00
Anarthal (Rubén Pérez)
033f6aaa62 Moves all logging logic to FSMs (#335)
Replaces connection_logger by buffered_logger and log_utils.hpp
Adds a system to log arbitrary values without defining new logger methods
2025-10-22 11:59:59 +02:00
Anarthal (Rubén Pérez)
42411be444 Changes request default's cancel_on_connection_lost to false (#334)
Changes request default constructor to set cancel_on_connection_lost to false, matching request::config's default initializer
Overwrites this flag to true for the setup request
Adds unit tests for the latter
2025-10-20 16:37:02 +02:00
Anarthal (Rubén Pérez)
6be0d122fb Moves the setup request execution to run_fsm (#333)
Adds unit tests to cover setup request execution in run_fsm
Entails no functional change
2025-10-20 15:56:46 +02:00
Anarthal (Rubén Pérez)
2b09ecbd78 Makes health checks flexible so they don't tear down connections under heavy load (#328)
Adds error::write_timeout

close #104
2025-10-20 15:29:20 +02:00
Anarthal (Rubén Pérez)
da09787d29 Moves logging into reader_fsm (#332)
* Removes logging all the reader actions, and logs specific messages inside the reader_fsm instead
* Adds constructors to reader actions
* Makes reader_fsm use connection_state
* Refactors reader_fsm tests
* Moves exec_fsm action printing to test code
2025-10-15 17:36:54 +02:00
Anarthal (Rubén Pérez)
f683e368dd Implements async_run as a FSM and adds tests (#330)
* Implements async_run as a FSM and adds tests
* Places all sans-io variables in connection_impl in a connection_state struct

Entails no functional change.
2025-10-13 22:19:39 +02:00
Anarthal (Rubén Pérez)
28ed27ce72 Changes cancel_on_connection_lost default to false and deprecates it (#329)
* Changes cancel_on_connection_lost default to false
* Deprecates cancel_on_connection_lost and cancel_if_not_connected
* Fixes a TODO in test_conn_exec_cancel

close #323
2025-10-11 13:30:41 +02:00
Anarthal (Rubén Pérez)
35fa68b926 Improves the cancellation discussion page (#327)
Fixes inaccuracies in request::config::cancel_if_unresponded
2025-10-09 12:11:43 +02:00
Anarthal (Rubén Pérez)
228b31917c Implements the writer as an FSM and adds tests (#325)
* Refactors the writer task into a FSM and adds unit tests.
* Adds a testing utility to check logging.

Entails no functional change (other than cosmetic word fixes to the logs).
2025-10-09 11:31:36 +02:00
Anarthal (Rubén Pérez)
d3e335942f Adds support for asio::cancel_after (#324)
* Adds support for asio::cancel_after in connection::{async_exec, async_run}
* Adds cancel_after tests
* Adds an example on using asio::cancel_after
* Adds a discussion page on timeouts and the `cancel_if_unresponded` flag

close #226
2025-10-06 18:11:25 +02:00
Anarthal (Rubén Pérez)
0c159280ba Implements connect as a FSM and fixes cancellation (#320)
Implements redis_stream::async_connect as a FSM
Adds per-operation cancellation handling code
Adds tests
2025-10-06 12:39:28 +02:00
Anarthal (Rubén Pérez)
1812be87bf Makes the CI run part of the examples (#322)
Removes a conditional from CMake that was causing examples to never be run under CI.
2025-10-03 20:08:58 +02:00
Anarthal (Rubén Pérez)
5771128f2d Adds per-operation cancellation support to async_run and cleans up cancellation (#321)
* Adds support for terminal and partial cancellation to async_run.
* Makes basic_connection::cancel use per-operation cancellation under the hood.
* Fixes a number of race conditions during cancellation which could cause the cancellation to be ignored. This could happen if the cancellation is delivered while an async handler is pending execution.
* Deprecates operation::{resolve, connect, ssl_handshake, reconnection, health_check}, in favor of operation::run. Calling basic_connection::cancel with these values (excepting reconnection) is now equivalent to using operation::run.
* Fixes a problem in the health checker that caused ping timeouts to be reported as cancellations.
* Sanitizes how the parallel group computes its final error code.
* Simplifies the reader, writer and health checker to not care about connection cancellation. This is now the responsibility of the parallel group.
* Removes an unnecessary setup_cancellation action in the reader FSM.
* Adds documentation regarding per-operation cancellation to async_receive.
* Adds additional health checker tests.
* Adds async_run per-operation cancellation tests.
* Adds reader FSM cancellation tests.
* Makes test_conn_exec_retry tests more resilient.
* Removes leftovers in the UNIX and TLS reconnection tests. These were required due to race conditions that have already been fixed.

close #318 
close #319
2025-10-03 18:48:30 +02:00
Marcelo
2babb79425 Merge pull request #311 from boostorg/refactoring_clean_code
Simplifies the read_buffer and add rotated bytes to usage.
2025-09-29 21:50:19 +02:00
Marcelo Zimbres
a70bdf6574 Simplifies the read_buffer and adds rotated bytes to usage.
Data rotation in the read buffer creates latency, we know it
is preset but so far its magnitude was unknown. This PR adds
it as a new field to the usage struct. For example, the
test_conn_echo_stress outputs now

   Commands sent: 780,002
   Bytes sent: 32,670,085
   Responses received: 780,001
   Pushes received: 750,001
   Bytes received (response): 3,210,147
   Bytes received (push): 32,250,036
   Bytes rotated: 3,109,190,184

In total approximately 34Mb are received but 3Gb are
rotated.
2025-09-28 13:02:12 +02:00
Anarthal (Rubén Pérez)
e414b3941a Simplifies the health checker (#317)
Modifies the health checker to use asio::cancel_after rather than a separate parallel group task
2025-09-27 19:08:19 +02:00
Anarthal (Rubén Pérez)
beab3f69ed Reduces the time elapsed by test_conn_health_check and adds test_conn_monitor (#316)
Splits test_conn_health_check into a test to verify that the health checker works and another to verify that MONITOR is handled correctly.
Tests no longer wait or use CLIENT PAUSE
2025-09-27 18:13:02 +02:00
Anarthal (Rubén Pérez)
f955dc01d2 Adds reliable cancellation support to async_exec (#310)
* Terminal cancellation in async_exec no longer tears down the connection when the cancelled request has been sent to the server.
* Adds support for partial cancellation in async_exec with the same semantics.
2025-09-26 12:50:42 +02:00
Anarthal (Rubén Pérez)
bcf120bd8f Increases antora log level and fixes a broken link (#315)
close #313
2025-09-24 12:53:42 +02:00
Anarthal (Rubén Pérez)
203e9298ed Fixes a race condition when cancelling requests on connection lost (#309)
Changes how cancel_on_conn_lost is used to ensure it is called only once and after the reader and writer tasks have exited.
This fixes a problem in test_conn_reconnect
Adds a test for multiplexer::reset()
Adds stronger invariants to the multiplexer functions to be called by the reader and writer
Removes test_issue_181, since the same functionality is being covered by unit tests already
Removes basic_connection::run_is_canceled
2025-09-22 13:04:28 +02:00
Anarthal (Rubén Pérez)
8da18379ba Replaces tribool by an enum and adds coverage for multiplexer (#301)
* In the context of the multiplexer, replaces tribool by consume_result to enhance readability and make values smaller
* Splits the multiplexer tests out of test_low_level_sync_sans_io into a separate test file
* Increases testing coverage for the multiplexer class

Entails no functional change.
2025-09-18 13:55:29 +02:00
Anarthal (Rubén Pérez)
40417a13b2 Deprecates request::config::hello_with_priority (#305) 2025-09-16 00:49:33 +02:00
Anarthal (Rubén Pérez)
74909be47d Removes CMakePresets.json (#304) 2025-09-15 14:19:04 +02:00
Anarthal (Rubén Pérez)
6a1a07f95a Adds custom setup requests (#303)
Adds config::setup and config::use_setup, to run arbitrary Redis commands on connection establishment
Improves docs for config::{username, password, clientname, database_index}
Splits all connection establishment tests into test_conn_hello

close #302
2025-09-15 14:15:11 +02:00
Anarthal (Rubén Pérez)
0cf2441ed2 Removes handshaker in favor of asio::deferred (#291)
Refactors the handshake process to use asio::deferred instead of a custom composed operation.
Fixes logging on HELLO error (close #297)
Fixes a potential problem on reconnection after a HELLO error is encountered (close #290)
Fixes a race condition in the health checker that could cause it to never exit on cancellation
Adds support for users with a username different than "default" and an empty password (close #298)
Adds integration testing for authentication
Adds unit testing for the hello utility functions
2025-09-04 16:48:00 +02:00
Anarthal (Rubén Pérez)
2133ed747b Fixes an exception when parsing intercalated errors and success messages into generic_response (#289)
close #287
2025-09-01 20:18:35 +02:00
Anarthal (Rubén Pérez)
1f6c6bd64d Adds Valkey CIs and docs (#296)
* Adds Valkey CIs

Modifies existing builds to use different database flavors and versions:
* Redis 7.4.5
* Redis 8.2.1
* Valkey 8.1.3

* Update docs
2025-09-01 20:14:26 +02:00
Marcelo
da2f0101d0 Merge pull request #286 from boostorg/parser_events
Add parse event init, node and done.
2025-08-10 17:04:26 +02:00
Marcelo Zimbres
a76a621b0b More code review changes. 2025-08-02 13:38:19 +02:00
Marcelo Zimbres
16bf57cf33 Add parse event init, node and done. 2025-07-26 22:44:53 +02:00
Anarthal (Rubén Pérez)
88d8f3c0ca Makes all objects in connection have a stable address (#285)
Moves read_buffer memory reservation to the connection's constructor
Makes read_buffer memory reservation size be a power of 2
2025-07-25 22:51:14 +02:00
Marcelo
20ab2c7e70 Merge pull request #283 from boostorg/refactoring_clean_code
Removes async_append_some
2025-07-22 20:51:50 +02:00
Marcelo Zimbres
8ee2213efe Code review changes. 2025-07-20 18:38:02 +02:00
Marcelo Zimbres
97d71d1d6b Removes async_append_some. 2025-07-17 12:37:17 +02:00
131 changed files with 15522 additions and 3111 deletions

View File

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

View File

@@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT)
test
json
endian
compat
)
foreach(dep IN LISTS deps)

View File

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

View File

@@ -87,9 +87,9 @@ 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
@@ -99,26 +99,25 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
generic_response resp;
flat_tree resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req, ignore);
co_await conn->async_exec(req);
// Loop reading Redis pushes.
for (;;) {
error_code ec;
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
for (error_code ec;;) {
co_await conn->async_receive2(resp, redirect_error(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);
resp.clear();
}
}
}
@@ -126,4 +125,4 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
## 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).

View File

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

View File

@@ -1,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[]

View File

@@ -0,0 +1,79 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
= Cancellation and timeouts
By default, running a request with xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`]
will wait until a connection to the Redis server is established by `async_run`.
This may take a very long time if the server is down.
For this reason, it is usually a good idea to set a timeout to `async_exec`
operations using the
https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_after.html[`asio::cancel_after`]
completion token:
[source,cpp]
----
using namespace std::chrono_literals;
// Compose a request with a SET command
request req;
req.push("SET", "my_key", 42);
// If the request hasn't completed after 10 seconds, it will be cancelled
// and an exception will be thrown.
co_await conn.async_exec(req, ignore, asio::cancel_after(10s));
----
See our {site-url}/example/cpp20_timeouts.cpp[example on timeouts]
for a full code listing.
You can also use `cancel_after` with other completion styles, like
callbacks and futures.
`cancel_after` works because `async_exec` supports the per-operation
cancellation mechanism. This is used by Boost.Asio to implement features
like `cancel_after` and parallel groups. All asynchronous operations
in the library support this mechanism. Please consult the documentation
for individual operations for more info.
== Retrying idempotent requests
We mentioned that `async_exec` waits until the server is up
before sending the request. But what happens if there is a communication
error after sending the request, but before receiving a response?
In this situation there is no way to know if the request was processed by the server or not.
By default, the library will consider the request as failed,
and `async_exec` will complete with an `asio::error::operation_aborted`
error code.
Some requests can be executed several times and result in the same outcome
as executing them only once. We say that these requests are _idempotent_.
The `SET` command is idempotent, while `INCR` is not.
If you know that a `request` object contains only idempotent commands,
you can instruct Boost.Redis to retry the request on failure, even
if the library is unsure about the server having processed the request or not.
You can do so by setting `cancel_if_unresponded`
in xref:reference:boost/redis/request/config.adoc[`request::config`]
to false:
[source,cpp]
----
// Compose a request
request req;
req.push("SET", "my_key", 42); // idempotent
req.get_config().cancel_if_unresponded = false; // Retry the request even if it was written but not responded
// Makes sure that the key is set, even in the presence of network errors.
// This may suspend for an unspecified period of time if the server is down.
co_await conn.async_exec(req, ignore);
----

View File

@@ -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])

View File

@@ -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.

View File

@@ -8,7 +8,8 @@
[#intro]
= Introduction
Boost.Redis is a high-level https://redis.io/[Redis] client library built on top of
Boost.Redis is a high-level https://redis.io/[Redis] and https://valkey.io/[Valkey]
client library built on top of
https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio]
that implements the Redis protocol
https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md[RESP3].
@@ -19,7 +20,8 @@ The requirements for using Boost.Redis are:
* Boost 1.84 or higher. Boost.Redis is included in Boost installations since Boost 1.84.
* pass:[C++17] or higher. Supported compilers include gcc 11 and later, clang 11 and later, and Visual Studio 16 (2019) and later.
* Redis 6 or higher (must support RESP3).
* Redis 6 or higher, or Valkey (any version). The database server must support RESP3.
We intend to maintain compatibility with both Redis and Valkey in the long-run.
* OpenSSL.
The documentation assumes basic-level knowledge about https://redis.io/docs/[Redis] and https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio].
@@ -95,9 +97,9 @@ 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]
@@ -108,26 +110,25 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
generic_response resp;
flat_tree resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req, ignore);
co_await conn->async_exec(req);
// Loop reading Redis pushes.
for (;;) {
error_code ec;
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
for (error_code ec;;) {
co_await conn->async_receive2(resp, redirect_error(ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
// Use the response resp in some way and then clear it.
// Use the response here and then clear it.
...
consume_one(resp);
resp.clear();
}
}
}
@@ -137,6 +138,7 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
Here is a list of topics that you might be interested in:
* xref:cancellation.adoc[Setting timeouts to requests and managing cancellation].
* xref:requests_responses.adoc[More on requests and responses].
* xref:serialization.adoc[Serializing and parsing into custom types].
* xref:logging.adoc[Configuring logging].

View File

@@ -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`]
|===

View File

@@ -37,7 +37,7 @@ req.push_range("HSET", "key", map);
Sending a request to Redis is performed by
xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`]
as already stated. Requests accept a xref:reference:boost/redis/request/config[`boost::redis::request::config`]
as already stated. Requests accept a xref:reference:boost/redis/request/config.adoc[`boost::redis::request::config`]
object when constructed that dictates how requests are handled in situations like
reconnection. The reader is advised to read it carefully.
@@ -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`.

View 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.

View File

@@ -17,9 +17,7 @@ endmacro()
macro(make_testable_example EXAMPLE_NAME STANDARD)
make_example(${EXAMPLE_NAME} ${STANDARD})
if (BOOST_REDIS_INTEGRATION_TESTS)
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME})
endif()
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379)
endmacro()
make_testable_example(cpp17_intro 17)
@@ -28,13 +26,14 @@ make_testable_example(cpp17_intro_sync 17)
make_testable_example(cpp20_intro 20)
make_testable_example(cpp20_containers 20)
make_testable_example(cpp20_json 20)
make_testable_example(cpp20_intro_tls 20)
make_testable_example(cpp20_unix_sockets 20)
make_testable_example(cpp20_timeouts 20)
make_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")

View File

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

View File

@@ -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)
@@ -7,8 +7,10 @@
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/signal_set.hpp>
@@ -28,11 +30,9 @@ 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;
@@ -45,20 +45,24 @@ auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
generic_response resp;
generic_flat_response resp;
conn->set_receive_response(resp);
while (conn->will_reconnect()) {
// Subscribe to channels.
co_await conn->async_exec(req, ignore);
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));
co_await conn->async_receive2(redirect_error(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;
for (auto const& elem: resp.value().get_view())
std::cout << elem.value << "\n";
std::cout << std::endl;
resp.value().clear();
}
}
@@ -72,7 +76,7 @@ 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);
}
}

View File

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

View File

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

View File

@@ -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)

View 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)

View File

@@ -1,11 +1,10 @@
/* 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/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
@@ -22,12 +21,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;
@@ -54,30 +49,29 @@ 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
while (conn->will_reconnect()) {
// Reconnect to the channels.
co_await conn->async_exec(req, ignore);
co_await conn->async_exec(req);
// Loop reading Redis pushs messages.
// Loop to read Redis push 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));
}
// Wait for pushes
co_await conn->async_receive2(asio::redirect_error(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;
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem: resp.value().get_view())
std::cout << elem.value << "\n";
consume_one(resp);
std::cout << std::endl;
resp.value().clear();
}
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -12,6 +12,8 @@
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/response.hpp>
#include <boost/assert.hpp>
@@ -147,8 +149,12 @@ public:
explicit general_aggregate(Result* c = nullptr)
: result_(c)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
@@ -160,16 +166,109 @@ 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() { }
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() { }
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:
@@ -180,8 +279,11 @@ public:
: result_(t)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
@@ -206,8 +308,11 @@ class simple_impl {
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
{
if (is_aggregate(node.data_type)) {
ec = redis::error::expects_resp3_simple_type;
@@ -226,8 +331,11 @@ private:
public:
void on_value_available(Result& result) { hint_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (nd.data_type != resp3::type::set)
@@ -257,8 +365,11 @@ private:
public:
void on_value_available(Result& result) { current_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (element_multiplicity(nd.data_type) != 2)
@@ -292,8 +403,11 @@ class vector_impl {
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
auto const m = element_multiplicity(nd.data_type);
@@ -313,8 +427,11 @@ private:
public:
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (i_ != -1) {
@@ -344,8 +461,11 @@ template <class Result>
struct list_impl {
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (!is_aggregate(nd.data_type)) {
BOOST_ASSERT(nd.aggregate_size == 1);
@@ -468,8 +588,11 @@ public:
}
}
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
@@ -480,7 +603,7 @@ public:
return;
BOOST_ASSERT(result_);
impl_(result_->value(), nd, ec);
impl_.on_node(result_->value(), nd, ec);
}
};
@@ -514,8 +637,11 @@ public:
: result_(o)
{ }
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
@@ -533,7 +659,7 @@ public:
impl_.on_value_available(result_->value().value());
}
impl_(result_->value().value(), nd, ec);
impl_.on_node(result_->value().value(), nd, ec);
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,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,31 +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 a socket read, in bytes.
/** @brief Maximum size of the socket read-buffer in bytes.
*
* Sets a limit on how much data is allowed to be read into the
* read buffer. It can be used to prevent DDOS.
* 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 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

View 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

View 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

View File

@@ -1,54 +0,0 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_CONNECTION_LOGGER_HPP
#define BOOST_REDIS_CONNECTION_LOGGER_HPP
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
#include <string_view>
namespace boost::redis::detail {
// Wraps a logger and a string buffer for re-use, and provides
// utility functions to format the log messages that we use.
// The long-term trend will be moving most of this class to finite state
// machines as we write them
class connection_logger {
logger logger_;
std::string msg_;
public:
connection_logger(logger&& logger) noexcept
: logger_(std::move(logger))
{ }
void reset(logger&& logger) { logger_ = std::move(logger); }
void on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res);
void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep);
void on_connect(system::error_code const& ec, std::string_view unix_socket_ep);
void on_ssl_handshake(system::error_code const& ec);
void on_write(system::error_code const& ec, std::size_t n);
void on_fsm_resume(reader_fsm::action const& action);
void on_hello(system::error_code const& ec, generic_response const& resp);
void log(logger::level lvl, std::string_view msg);
void log(logger::level lvl, std::string_view op, system::error_code const& ec);
void trace(std::string_view message) { log(logger::level::debug, message); }
void trace(std::string_view op, system::error_code const& ec)
{
log(logger::level::debug, op, ec);
}
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_LOGGER_HPP

View File

@@ -0,0 +1,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)
//
#ifndef BOOST_REDIS_CONNECTION_STATE_HPP
#define BOOST_REDIS_CONNECTION_STATE_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/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 ping_req{};
// 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

View File

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

View File

@@ -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

View File

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

View File

@@ -1,34 +0,0 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_HELPER_HPP
#define BOOST_REDIS_HELPER_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail {
template <class T>
auto is_cancelled(T const& self)
{
return self.get_cancellation_state().cancelled() != asio::cancellation_type_t::none;
}
#define BOOST_REDIS_CHECK_OP0(X) \
if (ec || redis::detail::is_cancelled(self)) { \
X self.complete(!!ec ? ec : asio::error::operation_aborted); \
return; \
}
#define BOOST_REDIS_CHECK_OP1(X) \
if (ec || redis::detail::is_cancelled(self)) { \
X self.complete(!!ec ? ec : asio::error::operation_aborted, {}); \
return; \
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_HELPER_HPP

View File

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

View File

@@ -0,0 +1,70 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_READ_BUFFER_HPP
#define BOOST_REDIS_READ_BUFFER_HPP
#include <boost/core/span.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string_view>
#include <utility>
#include <vector>
namespace boost::redis::detail {
class read_buffer {
public:
using span_type = span<char>;
struct consume_result {
std::size_t consumed;
std::size_t rotated;
};
// See config.hpp for the meaning of these parameters.
struct config {
std::size_t read_buffer_append_size = 4096u;
std::size_t max_read_size = static_cast<std::size_t>(-1);
};
// Prepare the buffer to receive more data.
[[nodiscard]]
auto prepare() -> system::error_code;
[[nodiscard]]
auto get_prepared() noexcept -> span_type;
void commit(std::size_t read_size);
[[nodiscard]]
auto get_commited() const noexcept -> std::string_view;
void clear();
// Consumes committed data by rotating the remaining data to the
// front of the buffer.
auto consume(std::size_t size) -> consume_result;
void reserve(std::size_t n);
friend bool operator==(read_buffer const& lhs, read_buffer const& rhs);
friend bool operator!=(read_buffer const& lhs, read_buffer const& rhs);
void set_config(config const& cfg) noexcept { cfg_ = cfg; };
private:
config cfg_ = config{};
std::vector<char> buffer_;
std::size_t offset_ = 0;
std::size_t append_buf_begin_ = 0;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_READ_BUFFER_HPP

View File

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

View File

@@ -8,8 +8,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

View File

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

View File

@@ -0,0 +1,29 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_RESP3_TYPE_TO_ERROR_HPP
#define BOOST_RESP3_TYPE_TO_ERROR_HPP
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert.hpp>
namespace boost::redis::detail {
inline error resp3_type_to_error(resp3::type t)
{
switch (t) {
case resp3::type::simple_error: return error::resp3_simple_error;
case resp3::type::blob_error: return error::resp3_blob_error;
case resp3::type::null: return error::resp3_null;
default: BOOST_ASSERT_MSG(false, "Unexpected data type."); return error::resp3_simple_error;
}
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP

View File

@@ -0,0 +1,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

View 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

View File

@@ -0,0 +1,92 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef BOOST_REDIS_WRITER_FSM_HPP
#define BOOST_REDIS_WRITER_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <cstddef>
// Sans-io algorithm for the writer task, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
// What should we do next?
enum class writer_action_type
{
done, // Call the final handler
write_some, // Issue a write on the stream
wait, // Wait until there is data to be written
};
class writer_action {
writer_action_type type_;
union {
system::error_code ec_;
std::chrono::steady_clock::duration timeout_;
};
writer_action(writer_action_type type, std::chrono::steady_clock::duration t) noexcept
: type_{type}
, timeout_{t}
{ }
public:
writer_action_type type() const { return type_; }
writer_action(system::error_code ec) noexcept
: type_{writer_action_type::done}
, ec_{ec}
{ }
static writer_action write_some(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::write_some, timeout};
}
static writer_action wait(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::wait, timeout};
}
system::error_code error() const
{
BOOST_ASSERT(type_ == writer_action_type::done);
return ec_;
}
std::chrono::steady_clock::duration timeout() const
{
BOOST_ASSERT(type_ == writer_action_type::write_some || type_ == writer_action_type::wait);
return timeout_;
}
};
class writer_fsm {
int resume_point_{0};
public:
writer_fsm() = default;
writer_action resume(
connection_state& st,
system::error_code ec,
std::size_t bytes_written,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -68,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.
@@ -88,6 +88,30 @@ enum class error
/// The configuration specified UNIX sockets with SSL, which is not supported.
unix_sockets_ssl_unsupported,
/// Reading data from the socket would exceed the maximum size allowed of the read buffer.
exceeds_maximum_read_buffer_size,
/// Timeout while writing data to the server.
write_timeout,
/// 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,
};
/**

View 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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

View File

@@ -0,0 +1,189 @@
//
// 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 <algorithm>
#include <cstddef>
#include <cstring>
#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;
}
// 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_}
{
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_;
}
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
{
data_.size = 0u;
view_tree_.clear();
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,
});
}
bool operator==(flat_tree const& a, flat_tree const& b)
{
// data is already taken into account by comparing the nodes.
return a.view_tree_ == b.view_tree_ && a.total_msgs_ == b.total_msgs_;
}
} // namespace boost::redis::resp3

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef BOOST_REDIS_IS_TERMINAL_CANCEL_HPP
#define BOOST_REDIS_IS_TERMINAL_CANCEL_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail {
constexpr bool is_terminal_cancel(asio::cancellation_type_t cancel_state)
{
return (cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none;
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,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

View File

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

View File

@@ -0,0 +1,89 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/assert.hpp>
#include <boost/core/make_span.hpp>
#include <utility>
namespace boost::redis::detail {
system::error_code read_buffer::prepare()
{
BOOST_ASSERT(append_buf_begin_ == buffer_.size());
auto const new_size = append_buf_begin_ + cfg_.read_buffer_append_size;
if (new_size > cfg_.max_read_size) {
return error::exceeds_maximum_read_buffer_size;
}
buffer_.resize(new_size);
return {};
}
void read_buffer::commit(std::size_t read_size)
{
BOOST_ASSERT(buffer_.size() >= (append_buf_begin_ + read_size));
buffer_.resize(append_buf_begin_ + read_size);
append_buf_begin_ = buffer_.size();
}
auto read_buffer::get_prepared() noexcept -> span_type
{
auto const size = buffer_.size();
return make_span(buffer_.data() + append_buf_begin_, size - append_buf_begin_);
}
auto read_buffer::get_commited() const noexcept -> std::string_view
{
return {buffer_.data() + offset_, append_buf_begin_ - offset_};
}
void read_buffer::clear()
{
buffer_.clear();
offset_ = 0;
append_buf_begin_ = 0;
}
read_buffer::consume_result
read_buffer::consume(std::size_t size)
{
// For convenience, if the requested size is larger than the
// committed buffer we cap it to the maximum.
auto const consumable = append_buf_begin_ - offset_;
if (size > consumable)
size = consumable;
offset_ += size;
BOOST_ASSERT(offset_ <= append_buf_begin_);
auto rotated = 0u;
if (offset_ >= 10'000'000 && size > 0u) {
buffer_.erase(buffer_.begin(), buffer_.begin() + offset_);
rotated = buffer_.size();
BOOST_ASSERT(offset_ <= append_buf_begin_);
append_buf_begin_ -= offset_;
offset_ = 0u;
}
return {size, rotated};
}
void read_buffer::reserve(std::size_t n) { buffer_.reserve(n); }
bool operator==(read_buffer const& lhs, read_buffer const& rhs)
{
return lhs.buffer_ == rhs.buffer_ && lhs.append_buf_begin_ == rhs.append_buf_begin_;
}
bool operator!=(read_buffer const& lhs, read_buffer const& rhs) { return !(lhs == rhs); }
} // namespace boost::redis::detail

View File

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

View File

@@ -18,7 +18,23 @@ 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)
{
payload_ += other.payload_;
commands_ += other.commands_;
expected_responses_ += other.expected_responses_;
}

View File

@@ -1,26 +0,0 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/detail/resp3_handshaker.hpp>
namespace boost::redis::detail {
void push_hello(config const& cfg, request& req)
{
if (!cfg.username.empty() && !cfg.password.empty() && !cfg.clientname.empty())
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password, "SETNAME", cfg.clientname);
else if (cfg.password.empty() && cfg.clientname.empty())
req.push("HELLO", "3");
else if (cfg.clientname.empty())
req.push("HELLO", "3", "AUTH", cfg.username, cfg.password);
else
req.push("HELLO", "3", "SETNAME", cfg.clientname);
if (cfg.database_index && cfg.database_index.value() != 0)
req.push("SELECT", cfg.database_index.value());
}
} // namespace boost::redis::detail

View File

@@ -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);
}

View File

@@ -0,0 +1,229 @@
//
// 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_;
}
// Compose the setup request. This only depends on the config, so it can be done just once
compose_setup_request(st.cfg);
// Compose the PING request. Same as above
compose_ping_request(st.cfg, st.ping_req);
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();
// Add the setup request to the multiplexer
if (st.cfg.setup.get_commands() != 0u) {
auto elm = make_elem(st.cfg.setup, 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

View 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

View File

@@ -0,0 +1,278 @@
//
// 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>
#include <algorithm>
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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,6 @@
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <algorithm>
#include <string>
#include <tuple>
@@ -21,7 +20,8 @@ namespace boost::redis {
namespace detail {
auto has_response(std::string_view cmd) -> bool;
}
struct request_access;
} // namespace detail
/** @brief Represents a Redis request.
*
@@ -33,11 +33,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 +44,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 +95,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 +110,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_;
}
@@ -123,14 +143,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 +162,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 +193,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
*/
@@ -226,23 +261,38 @@ public:
* { "channel1" , "channel2" , "channel3" };
*
* request req;
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
* req.push("SUBSCRIBE", channels.cbegin(), channels.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);
* SUBSCRIBE channel1 channel2 channel3
* @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 +323,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 +365,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 +401,17 @@ 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);
private:
void check_cmd(std::string_view cmd)
{
@@ -332,8 +429,22 @@ private:
std::size_t commands_ = 0;
std::size_t expected_responses_ = 0;
bool has_hello_priority_ = false;
friend struct detail::request_access;
};
namespace detail {
struct request_access {
inline static void set_priority(request& r, bool value) { r.has_hello_priority_ = value; }
inline static bool has_priority(const request& r) { return r.has_hello_priority_; }
};
// Creates a HELLO 3 request
request make_hello_request();
} // namespace detail
} // namespace boost::redis
#endif // BOOST_REDIS_REQUEST_HPP

View File

@@ -0,0 +1,252 @@
//
// 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 <cstddef>
#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. Call @ref get_view to access the actual RESP3 nodes.
* Once populated, `flat_tree` can't be modified, except for @ref clear and assignment.
*
* 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 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
* 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
* References to the nodes and strings in `other` remain valid.
* 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
* 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);
friend bool operator==(flat_tree const&, flat_tree const&);
friend bool operator!=(flat_tree const&, flat_tree const&);
/** @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
* @ref get_view return empty 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_.size; }
/** @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 a vector with the nodes in the tree.
*
* This is the main way to access the contents of the tree.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The nodes in the tree.
*/
auto get_view() const noexcept -> view_tree const& { return view_tree_; }
/** @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;
void notify_done() { ++total_msgs_; }
// 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;
};
/**
* @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

View File

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

View File

@@ -8,6 +8,8 @@
#include <boost/assert.hpp>
#include <ostream>
namespace boost::redis::resp3 {
auto to_string(type t) noexcept -> char const*

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -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>;

View File

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

View File

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

View File

@@ -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

View File

@@ -9,9 +9,8 @@
#include <boost/assert.hpp>
#include <ostream>
#include <string>
#include <vector>
#include <cstddef>
#include <iosfwd>
namespace boost::redis::resp3 {

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -8,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.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

View File

@@ -4,17 +4,22 @@
* 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/ignore.ipp>
#include <boost/redis/impl/logger.ipp>
#include <boost/redis/impl/multiplexer.ipp>
#include <boost/redis/impl/read_buffer.ipp>
#include <boost/redis/impl/reader_fsm.ipp>
#include <boost/redis/impl/request.ipp>
#include <boost/redis/impl/resp3_handshaker.ipp>
#include <boost/redis/impl/response.ipp>
#include <boost/redis/impl/run_fsm.ipp>
#include <boost/redis/impl/sentinel_resolve_fsm.ipp>
#include <boost/redis/impl/writer_fsm.ipp>
#include <boost/redis/impl/flat_tree.ipp>
#include <boost/redis/resp3/impl/parser.ipp>
#include <boost/redis/resp3/impl/serialization.ipp>
#include <boost/redis/resp3/impl/type.ipp>

View File

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

View File

@@ -15,7 +15,7 @@ target_compile_features(boost_redis_src PRIVATE cxx_std_17)
target_link_libraries(boost_redis_src PRIVATE boost_redis_project_options)
# Test utils
add_library(boost_redis_tests_common STATIC common.cpp)
add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp)
target_compile_features(boost_redis_tests_common PRIVATE cxx_std_17)
target_link_libraries(boost_redis_tests_common PRIVATE boost_redis_project_options)
@@ -35,30 +35,48 @@ 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_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_read_buffer)
# Tests that require a real Redis server
make_test(test_conn_quit)
make_test(test_conn_tls)
make_test(test_conn_exec_retry)
make_test(test_conn_exec_error)
make_test(test_run)
make_test(test_conn_run_cancel)
make_test(test_conn_check_health)
make_test(test_conn_exec)
make_test(test_conn_push)
make_test(test_conn_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(

View File

@@ -42,6 +42,7 @@ lib redis_test_common
:
boost_redis.cpp
common.cpp
sansio_utils.cpp
: requirements $(requirements)
: usage-requirements $(requirements)
;
@@ -51,12 +52,25 @@ 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_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_read_buffer
;
# Build and run the tests

View File

@@ -1,11 +1,16 @@
#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 <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string_view>
namespace net = boost::asio;
@@ -50,7 +55,6 @@ boost::redis::config make_test_config()
{
boost::redis::config cfg;
cfg.addr.host = get_server_hostname();
cfg.max_read_size = 1000000;
return cfg;
}
@@ -69,3 +73,60 @@ void run_coroutine_test(net::awaitable<void> op, std::chrono::steady_clock::dura
throw std::runtime_error("Coroutine test did not finish");
}
#endif // BOOST_ASIO_HAS_CO_AWAIT
// Finds a value in the output of the CLIENT INFO command
// format: key1=value1 key2=value2
std::string_view find_client_info(std::string_view client_info, std::string_view key)
{
std::string prefix{key};
prefix += '=';
auto const pos = client_info.find(prefix);
if (pos == std::string_view::npos)
return {};
auto const pos_begin = pos + prefix.size();
auto const pos_end = client_info.find(' ', pos_begin);
return client_info.substr(pos_begin, pos_end - pos_begin);
}
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';
}};
}

View File

@@ -1,6 +1,8 @@
#pragma once
#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>
@@ -10,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
@@ -34,3 +38,12 @@ void run(
boost::redis::config cfg = make_test_config(),
boost::system::error_code ec = boost::asio::error::operation_aborted,
boost::redis::operation op = boost::redis::operation::receive);
// Finds a value in the output of the CLIENT INFO command
// format: key1=value1 key2=value2
std::string_view find_client_info(std::string_view client_info, std::string_view key);
// 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
View 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
View 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
View 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

View File

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

View File

@@ -0,0 +1,242 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/setup_request_utils.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/result.hpp>
namespace asio = boost::asio;
namespace redis = boost::redis;
using redis::detail::compose_setup_request;
using boost::system::error_code;
namespace {
void test_hello()
{
redis::config cfg;
cfg.clientname = "";
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_select()
{
redis::config cfg;
cfg.clientname = "";
cfg.database_index = 10;
compose_setup_request(cfg);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_clientname()
{
redis::config cfg;
compose_setup_request(cfg);
std::string_view const
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
compose_setup_request(cfg);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth_empty_password()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
compose_setup_request(cfg);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth_setname()
{
redis::config cfg;
cfg.clientname = "mytest";
cfg.username = "foo";
cfg.password = "bar";
compose_setup_request(cfg);
std::string_view const expected =
"*7\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$7\r\nSETNAME\r\n$"
"6\r\nmytest\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_use_setup()
{
redis::config cfg;
cfg.clientname = "mytest";
cfg.username = "foo";
cfg.password = "bar";
cfg.database_index = 4;
cfg.use_setup = true;
cfg.setup.push("SELECT", 8);
compose_setup_request(cfg);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
// Regression check: we set the priority flag
void test_use_setup_no_hello()
{
redis::config cfg;
cfg.use_setup = true;
cfg.setup.clear();
cfg.setup.push("SELECT", 8);
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
// Regression check: we set the relevant cancellation flags in the request
void test_use_setup_flags()
{
redis::config cfg;
cfg.use_setup = true;
cfg.setup.clear();
cfg.setup.push("SELECT", 8);
cfg.setup.get_config().cancel_if_unresponded = false;
cfg.setup.get_config().cancel_on_connection_lost = false;
compose_setup_request(cfg);
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
// When using Sentinel, a ROLE command is added. This works
// both with the old HELLO and new setup strategies.
void test_sentinel_auth()
{
redis::config cfg;
cfg.sentinel.addresses = {
{"localhost", "26379"}
};
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
compose_setup_request(cfg);
std::string_view const expected =
"*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
"*1\r\n$4\r\nROLE\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_sentinel_use_setup()
{
redis::config cfg;
cfg.sentinel.addresses = {
{"localhost", "26379"}
};
cfg.use_setup = true;
cfg.setup.push("SELECT", 42);
compose_setup_request(cfg);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n42\r\n"
"*1\r\n$4\r\nROLE\r\n";
BOOST_TEST_EQ(cfg.setup.payload(), expected);
BOOST_TEST(cfg.setup.has_hello_priority());
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
} // namespace
int main()
{
test_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_sentinel_auth();
test_sentinel_use_setup();
return boost::report_errors();
}

View File

@@ -0,0 +1,110 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/connection.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/experimental/channel_error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include "common.hpp"
using namespace std::chrono_literals;
namespace asio = boost::asio;
using boost::system::error_code;
using boost::redis::request;
using boost::redis::basic_connection;
using boost::redis::connection;
using boost::redis::ignore;
using boost::redis::generic_response;
namespace {
template <class Connection>
void test_run()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool run_finished = false;
// Call the function with a very short timeout
conn.async_run(make_test_config(), asio::cancel_after(1ms, [&](error_code ec) {
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
run_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
}
template <class Connection>
void test_exec()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool exec_finished = false;
request req;
req.push("PING", "cancel_after");
// Call the function with a very short timeout.
// The connection is not being run, so these can't succeed
conn.async_exec(req, ignore, asio::cancel_after(1ms, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
exec_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
}
template <class Connection>
void test_receive()
{
// Setup
asio::io_context ioc;
Connection conn{ioc};
bool receive_finished = false;
generic_response resp;
conn.set_receive_response(resp);
// Call the function with a very short timeout.
conn.async_receive(asio::cancel_after(1ms, [&](error_code ec, std::size_t) {
BOOST_TEST_EQ(ec, asio::experimental::channel_errc::channel_cancelled);
receive_finished = true;
}));
ioc.run_for(test_timeout);
BOOST_TEST(receive_finished);
}
} // namespace
int main()
{
test_run<basic_connection<asio::io_context::executor_type>>();
test_run<connection>();
test_exec<basic_connection<asio::io_context::executor_type>>();
test_exec<connection>();
test_receive<basic_connection<asio::io_context::executor_type>>();
test_receive<connection>();
return boost::report_errors();
}

View File

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

View File

@@ -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,44 @@ 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 +101,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,6 +129,10 @@ 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");
@@ -130,12 +140,8 @@ BOOST_AUTO_TEST_CASE(echo_stress)
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 +150,13 @@ 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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

121
test/test_conn_monitor.cpp Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More