mirror of
https://github.com/boostorg/redis.git
synced 2026-01-19 16:52:08 +00:00
Compare commits
209 Commits
v1.4.2
...
parser_eve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a76a621b0b | ||
|
|
16bf57cf33 | ||
|
|
88d8f3c0ca | ||
|
|
20ab2c7e70 | ||
|
|
8ee2213efe | ||
|
|
97d71d1d6b | ||
|
|
76129bbcb8 | ||
|
|
adf17f2b3b | ||
|
|
6499818911 | ||
|
|
963ae8d145 | ||
|
|
fe4ba64a97 | ||
|
|
b82224700e | ||
|
|
620b1e9510 | ||
|
|
f04d97ffa5 | ||
|
|
7304d99bf6 | ||
|
|
89a42dbf74 | ||
|
|
a39d130240 | ||
|
|
6d5b550bb3 | ||
|
|
4a2085c800 | ||
|
|
2fc54bc73b | ||
|
|
0c8c6fcc09 | ||
|
|
479068e778 | ||
|
|
35d50700b9 | ||
|
|
b58e4f94de | ||
|
|
e8b13bd7a0 | ||
|
|
c1c50e3e24 | ||
|
|
328ad97a79 | ||
|
|
1060733b84 | ||
|
|
f9d0679be5 | ||
|
|
11e54de8f8 | ||
|
|
109248d53b | ||
|
|
f7b4ec291e | ||
|
|
c4e31e5f2f | ||
|
|
f32da1b0e3 | ||
|
|
8b98081ccf | ||
|
|
77ade122ea | ||
|
|
746fb62619 | ||
|
|
ac4e6e29b3 | ||
|
|
412f5535ea | ||
|
|
31ceed9f8f | ||
|
|
e7c1b9ed5f | ||
|
|
35cfdae3f3 | ||
|
|
3a74575ada | ||
|
|
6cdbd64eb8 | ||
|
|
c21ac7569a | ||
|
|
7dd5a77d4f | ||
|
|
a53278aa2d | ||
|
|
6abef73cb1 | ||
|
|
9a48633bdf | ||
|
|
c615902559 | ||
|
|
d910557db4 | ||
|
|
5089eef376 | ||
|
|
8f4e980fd5 | ||
|
|
8df535f50e | ||
|
|
26197e8300 | ||
|
|
f592073330 | ||
|
|
32bc39a63a | ||
|
|
ca2c4efa35 | ||
|
|
7f9ac6f0fc | ||
|
|
d68c4583ed | ||
|
|
943cf0d740 | ||
|
|
53cabf0745 | ||
|
|
c309c5776b | ||
|
|
a535862d51 | ||
|
|
dfecf0bd0a | ||
|
|
ce4e5cbffa | ||
|
|
c370e930c5 | ||
|
|
e30bb373aa | ||
|
|
b8a52e5e61 | ||
|
|
302d50e262 | ||
|
|
86015cbdde | ||
|
|
84ead6a3e4 | ||
|
|
dbfb72d853 | ||
|
|
9039c3cd90 | ||
|
|
abde1afcb0 | ||
|
|
3adac158ab | ||
|
|
63aad808b6 | ||
|
|
90a07a1e07 | ||
|
|
585988bb61 | ||
|
|
b5e90ea5f4 | ||
|
|
42d1250b53 | ||
|
|
299e7ca3a8 | ||
|
|
0f56460056 | ||
|
|
6e7c42be7f | ||
|
|
6c6bcd03c0 | ||
|
|
3098fca125 | ||
|
|
25929578b9 | ||
|
|
1957cc7106 | ||
|
|
a8c4f7520f | ||
|
|
e8dd4d69eb | ||
|
|
d1822761a5 | ||
|
|
a4d2bb983d | ||
|
|
30ba3c3eb6 | ||
|
|
90f3d6cd92 | ||
|
|
ef6dea1666 | ||
|
|
90b001a54e | ||
|
|
ec1ca876eb | ||
|
|
1b90346b7c | ||
|
|
8f46d1eaa9 | ||
|
|
a4e9a60f34 | ||
|
|
e9b16a3140 | ||
|
|
ced4f9bd02 | ||
|
|
43878d68a3 | ||
|
|
29892f2837 | ||
|
|
b71dff6dd0 | ||
|
|
a9204fcc91 | ||
|
|
1788ebc80b | ||
|
|
5656571aa4 | ||
|
|
aa6622ffa2 | ||
|
|
f459d5d89b | ||
|
|
35276b6acb | ||
|
|
122ffb20b2 | ||
|
|
1f4b709b21 | ||
|
|
7d52065a87 | ||
|
|
f0d92c16a5 | ||
|
|
1df18258d8 | ||
|
|
71c60a5a89 | ||
|
|
098fbd68d2 | ||
|
|
4f2b12adbc | ||
|
|
e7cec45cb2 | ||
|
|
b19067cfed | ||
|
|
0af1c2e73d | ||
|
|
da48368d53 | ||
|
|
b6e1280075 | ||
|
|
5d553f5d71 | ||
|
|
5c46b62958 | ||
|
|
6cde6eab44 | ||
|
|
78792199ef | ||
|
|
f5793ac9bc | ||
|
|
dfc2bd1ac2 | ||
|
|
0445e74fa3 | ||
|
|
234f961e87 | ||
|
|
8bb0004188 | ||
|
|
4257b2eaec | ||
|
|
96da11a2cc | ||
|
|
3861c5de74 | ||
|
|
168ee6148a | ||
|
|
723e72797f | ||
|
|
7caea928af | ||
|
|
71b9a4f428 | ||
|
|
d89a976729 | ||
|
|
154d0b106d | ||
|
|
2b12525206 | ||
|
|
0bcbf6d4e4 | ||
|
|
6389daa783 | ||
|
|
ab2d6cdea8 | ||
|
|
63ce40e365 | ||
|
|
f2a005a8c4 | ||
|
|
0c06be66de | ||
|
|
0380e643ed | ||
|
|
ff734694ab | ||
|
|
548e3d4cb6 | ||
|
|
66b632b13d | ||
|
|
11c9c1b787 | ||
|
|
d6f9e435c7 | ||
|
|
9a7816dbf4 | ||
|
|
199fb6c261 | ||
|
|
4d30d1e0c0 | ||
|
|
92be6d958f | ||
|
|
14d3c0232e | ||
|
|
7412b37e08 | ||
|
|
60ba5b62af | ||
|
|
0303ae0dbc | ||
|
|
ea6c5536c1 | ||
|
|
d386b30c3a | ||
|
|
2951acc80f | ||
|
|
faf15fe7e8 | ||
|
|
7f3f8b0c13 | ||
|
|
f37e514961 | ||
|
|
b7b4f8f449 | ||
|
|
686cb306ea | ||
|
|
fcbe2c431c | ||
|
|
a7b3fbdd9a | ||
|
|
5ea0d3c467 | ||
|
|
2cd487784b | ||
|
|
b41e2704a1 | ||
|
|
765f0d45e8 | ||
|
|
84c8649d66 | ||
|
|
0bf4e76981 | ||
|
|
1d329df81b | ||
|
|
56f7d5af69 | ||
|
|
d0c3b3f7ee | ||
|
|
87ebc6cf4a | ||
|
|
ffc35e8e3e | ||
|
|
a02837ab33 | ||
|
|
4a39a0d20a | ||
|
|
56d9a2778f | ||
|
|
c732f33b48 | ||
|
|
221016f1c9 | ||
|
|
cb9fdba0a4 | ||
|
|
1c96a60709 | ||
|
|
b66d067af8 | ||
|
|
bc08a8d411 | ||
|
|
53ef947cf3 | ||
|
|
ecfe51c7ae | ||
|
|
be20c0d48c | ||
|
|
d5031c3f69 | ||
|
|
6748f7682a | ||
|
|
2a4936a9e1 | ||
|
|
4547e1ac07 | ||
|
|
44a608c0ba | ||
|
|
1ed8e0182c | ||
|
|
d8cf431dc2 | ||
|
|
401dd24419 | ||
|
|
509635f222 | ||
|
|
4fbd0c6853 | ||
|
|
b8899ecdc7 | ||
|
|
7d09040646 | ||
|
|
0de26fb0ce |
176
.clang-format
Normal file
176
.clang-format
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
Language: Cpp
|
||||
ColumnLimit: 100
|
||||
IndentWidth: 3
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: true
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: true
|
||||
AccessModifierOffset: -3
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
PointerAlignment: Left
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^<boost/redis.*>|<boost/redis/.*>'
|
||||
Priority: -10
|
||||
SortPriority: 1
|
||||
- Regex: '^<boost/.*>'
|
||||
Priority: -8
|
||||
SortPriority: 3
|
||||
- Regex: "^<.*"
|
||||
Priority: -6
|
||||
SortPriority: 5
|
||||
- Regex: ".*"
|
||||
Priority: -5
|
||||
SortPriority: 4
|
||||
IndentCaseLabels: true
|
||||
AllowShortCaseLabelsOnASingleLine: true
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlignArrayOfStructures: Left
|
||||
DerivePointerAlignment: false
|
||||
PenaltyBreakAssignment: 2000000
|
||||
PenaltyBreakBeforeFirstCallParameter: 0
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyBreakScopeResolution: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 200000000
|
||||
|
||||
# Defaults (based on Google)
|
||||
AlignConsecutiveMacros: false
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: ExceptShortType
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BreakTemplateDeclarations: Leave
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: true
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
CommentPragmas: '(^ IWYU pragma:)|(^\\copydoc )|(^ ?\\n)'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 0
|
||||
ContinuationIndentWidth: 3
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: true
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeIsMainRegex: null
|
||||
IncludeIsMainSourceRegex: ""
|
||||
IndentGotoLabels: false
|
||||
IndentPPDirectives: None
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Never
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
RawStringFormats:
|
||||
- Language: Cpp
|
||||
Delimiters:
|
||||
- cc
|
||||
- CC
|
||||
- cpp
|
||||
- Cpp
|
||||
- CPP
|
||||
- "c++"
|
||||
- "C++"
|
||||
CanonicalDelimiter: ""
|
||||
BasedOnStyle: google
|
||||
- Language: TextProto
|
||||
Delimiters:
|
||||
- pb
|
||||
- PB
|
||||
- proto
|
||||
- PROTO
|
||||
EnclosingFunctions:
|
||||
- EqualsProto
|
||||
- EquivToProto
|
||||
- PARSE_PARTIAL_TEXT_PROTO
|
||||
- PARSE_TEST_PROTO
|
||||
- PARSE_TEXT_PROTO
|
||||
- ParseTextOrDie
|
||||
- ParseTextProtoOrDie
|
||||
CanonicalDelimiter: ""
|
||||
BasedOnStyle: google
|
||||
ReflowComments: false
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
Standard: Auto
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: true
|
||||
AlignCaseColons: false
|
||||
---
|
||||
|
||||
@@ -7,7 +7,7 @@ codecov:
|
||||
|
||||
ignore:
|
||||
- "benchmarks/cpp/asio/*"
|
||||
- "examples/*"
|
||||
- "example/*"
|
||||
- "tests/*"
|
||||
- "/usr/*"
|
||||
- "**/boost/*"
|
||||
|
||||
425
.github/workflows/ci.yml
vendored
425
.github/workflows/ci.yml
vendored
@@ -1,10 +1,18 @@
|
||||
# CI script to verify that CMake and B2 builds work.
|
||||
# B2 builds include only tests that don't require a DB server, to avoid race conditions.
|
||||
# CMake tests include the actual project tests and all the CMake integration workflows
|
||||
# recommended by Boost.CI.
|
||||
# Windows CMake jobs build the code but don't run the tests,
|
||||
# since we don't have a way to set up a Redis server on Windows (yet).
|
||||
# Subcommands are implemented by the tools/ci.py script in a platform-independent manner.
|
||||
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: "${{matrix.generator}} ${{matrix.toolset}} Boost ${{matrix.boost_version}} ${{matrix.build_type}} ${{matrix.name_args}}"
|
||||
windows-cmake:
|
||||
name: "CMake ${{matrix.toolset}} ${{matrix.build-type}} C++${{matrix.cxxstd}}"
|
||||
runs-on: ${{matrix.os}}
|
||||
defaults:
|
||||
run:
|
||||
@@ -12,84 +20,109 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
boost_version: ["1.81.0"]
|
||||
os: [windows-2019, windows-2022]
|
||||
toolset: [v142, v143]
|
||||
build_type: [Release]
|
||||
generator: ["Visual Studio 16 2019", "Visual Studio 17 2022"]
|
||||
config_args: [""]
|
||||
build_args: [""]
|
||||
name_args: [""]
|
||||
exclude:
|
||||
- { os: windows-2019, toolset: v143 }
|
||||
- { os: windows-2019, generator: "Visual Studio 17 2022" }
|
||||
- { os: windows-2022, generator: "Visual Studio 16 2019" }
|
||||
# The following combinations are not available through install-boost
|
||||
- { boost_version: "1.81.0", toolset: v143 }
|
||||
|
||||
include:
|
||||
#- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Debug', build-shared-libs: 1 }
|
||||
#- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Release', build-shared-libs: 0 }
|
||||
- { toolset: msvc-14.3, os: windows-2022, generator: "Visual Studio 17 2022", cxxstd: '20', build-type: 'Debug', build-shared-libs: 0 }
|
||||
- { toolset: msvc-14.3, os: windows-2022, generator: "Visual Studio 17 2022", cxxstd: '20', build-type: 'Release', build-shared-libs: 1 }
|
||||
env:
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: 4
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add boost toolset to environment
|
||||
if: contains(fromJson('["1.81.0"]'), matrix.boost_version)
|
||||
run: echo BOOST_TOOLSET=$(echo "msvc") >> $GITHUB_ENV
|
||||
- name: Setup Boost
|
||||
run: python3 tools/ci.py setup-boost --source-dir=$(pwd)
|
||||
|
||||
# The platform_version passed to boost-install determines the msvc toolset version for which static libs are installed.
|
||||
- name: Add boost platform version to environment
|
||||
- name: Build a Boost distribution using B2
|
||||
run: |
|
||||
declare -A toolset_to_platform_version=( [v142]="2019" [v143]="2022" )
|
||||
key=$(echo "${{matrix.toolset}}")
|
||||
echo BOOST_PLATFORM_VERSION="${toolset_to_platform_version[$key]}" >> $GITHUB_ENV
|
||||
python3 tools/ci.py build-b2-distro \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Add boost install path to environment
|
||||
run: echo BOOST_INSTALL_PATH="${GITHUB_WORKSPACE}/boost-${{matrix.boost_version}}${BOOST_TOOLSET}${BOOST_PLATFORM_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Add build type configuration to environment
|
||||
run: echo BUILD_CONFIG_ARG="--config ${{matrix.build_type}}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Boost installation
|
||||
id: cache-boost
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{env.BOOST_INSTALL_PATH}}
|
||||
key: ${{matrix.boost_version}}${{env.BOOST_TOOLSET}}${{env.BOOST_PLATFORM_VERSION}}
|
||||
|
||||
- name: Install Boost
|
||||
if: steps.cache-boost.outputs.cache-hit != 'true'
|
||||
uses: MarkusJx/install-boost@v2.4.1
|
||||
with:
|
||||
boost_version: ${{matrix.boost_version}}
|
||||
toolset: ${{env.BOOST_TOOLSET}}
|
||||
boost_install_dir: ${{env.BOOST_INSTALL_PATH}}
|
||||
platform_version: ${{env.BOOST_PLATFORM_VERSION}}
|
||||
arch: null
|
||||
|
||||
- name: Install openssl
|
||||
run: choco install openssl
|
||||
|
||||
- name: Create build directory
|
||||
run: mkdir build
|
||||
|
||||
- name: Configure
|
||||
working-directory: build
|
||||
- name: Build a Boost distribution using CMake
|
||||
run: |
|
||||
cmake -T "${{matrix.toolset}}" \
|
||||
-G "${{matrix.generator}}" \
|
||||
${{matrix.config_args}} \
|
||||
${BOOST_COMPILER_ARG}\
|
||||
"${GITHUB_WORKSPACE}"
|
||||
env:
|
||||
BOOST_ROOT: ${{env.BOOST_INSTALL_PATH}}/boost
|
||||
|
||||
- name: Build
|
||||
working-directory: build
|
||||
python3 tools/ci.py build-cmake-distro \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--generator "${{ matrix.generator }}" \
|
||||
--build-shared-libs ${{ matrix.build-shared-libs }}
|
||||
|
||||
- name: Build the project tests
|
||||
run: |
|
||||
cmake --build . ${BUILD_CONFIG_ARG} ${{matrix.build_args}}
|
||||
python3 tools/ci.py build-cmake-standalone-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--generator "${{ matrix.generator }}" \
|
||||
--build-shared-libs ${{ matrix.build-shared-libs }}
|
||||
|
||||
posix:
|
||||
# # TODO: re-enable this when a Redis server is available for this job
|
||||
# - name: Run the project tests
|
||||
# run: |
|
||||
# python3 tools/ci.py run-cmake-standalone-tests \
|
||||
# --build-type ${{ matrix.build-type }}
|
||||
|
||||
- name: Run add_subdirectory tests
|
||||
run: |
|
||||
python3 tools/ci.py run-cmake-add-subdirectory-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--generator "${{ matrix.generator }}" \
|
||||
--build-shared-libs ${{ matrix.build-shared-libs }}
|
||||
|
||||
- name: Run find_package tests with the built cmake distribution
|
||||
run: |
|
||||
python3 tools/ci.py run-cmake-find-package-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--generator "${{ matrix.generator }}" \
|
||||
--build-shared-libs ${{ matrix.build-shared-libs }}
|
||||
|
||||
- name: Run find_package tests with the built b2 distribution
|
||||
run: |
|
||||
python3 tools/ci.py run-cmake-b2-find-package-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--generator "${{ matrix.generator }}" \
|
||||
--build-shared-libs ${{ matrix.build-shared-libs }}
|
||||
|
||||
windows-b2:
|
||||
name: "B2 ${{matrix.toolset}}"
|
||||
runs-on: ${{matrix.os}}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
#- { toolset: msvc-14.2, os: windows-2019 }
|
||||
- { toolset: msvc-14.3, os: windows-2022 }
|
||||
env:
|
||||
OPENSSL_ROOT: "C:\\Program Files\\OpenSSL"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup user-config.jam
|
||||
run: cp tools/user-config.jam "${HOMEDRIVE}${HOMEPATH}/"
|
||||
|
||||
- name: Setup Boost
|
||||
run: python3 tools/ci.py setup-boost --source-dir=$(pwd)
|
||||
|
||||
- name: Build and run project tests using B2
|
||||
run: |
|
||||
python3 tools/ci.py run-b2-tests \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--cxxstd 17,20 \
|
||||
--variant debug,release
|
||||
|
||||
posix-cmake:
|
||||
name: "CMake ${{ matrix.toolset }} ${{ matrix.cxxstd }} ${{ matrix.build-type }} ${{ matrix.cxxflags }}"
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -98,39 +131,227 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxflags: '-std=c++17', ldflags: '' }
|
||||
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxflags: '-std=c++20', ldflags: '' }
|
||||
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxflags: '-std=c++17', ldflags: '' }
|
||||
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxflags: '-std=c++20', ldflags: '' }
|
||||
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxflags: '-std=c++17', ldflags: '' }
|
||||
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxflags: '-std=c++20', ldflags: '' }
|
||||
- { toolset: clang, compiler: clang++-14, install: 'clang-14 libc++-14-dev libc++abi-14-dev', os: ubuntu-22.04, cxxflags: '-std=c++17 -stdlib=libc++', ldflags: '-lc++' }
|
||||
- { toolset: clang, compiler: clang++-14, install: 'clang-14 libc++-14-dev libc++abi-14-dev', os: ubuntu-22.04, cxxflags: '-std=c++20 -stdlib=libc++', ldflags: '-lc++' }
|
||||
runs-on: ${{ matrix.os }}
|
||||
- toolset: gcc-11
|
||||
install: g++-11
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '17'
|
||||
build-type: 'Debug'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: gcc-11
|
||||
install: g++-11
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '20'
|
||||
build-type: 'Release'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: clang-11
|
||||
install: clang-11
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '17'
|
||||
build-type: 'Debug'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: clang-11
|
||||
install: clang-11
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '20'
|
||||
build-type: 'Debug'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: clang-13
|
||||
install: clang-13
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '17'
|
||||
build-type: 'Release'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: clang-13
|
||||
install: clang-13
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '20'
|
||||
build-type: 'Release'
|
||||
ldflags: ''
|
||||
|
||||
- toolset: clang-14
|
||||
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '17'
|
||||
build-type: 'Debug'
|
||||
cxxflags: '-stdlib=libc++'
|
||||
ldflags: '-lc++'
|
||||
|
||||
- toolset: clang-14
|
||||
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
|
||||
container: ubuntu:22.04
|
||||
cxxstd: '20'
|
||||
build-type: 'Release'
|
||||
cxxflags: '-stdlib=libc++'
|
||||
ldflags: '-lc++'
|
||||
|
||||
- toolset: clang-19
|
||||
install: 'clang-19'
|
||||
container: ubuntu:24.04
|
||||
cxxstd: '23'
|
||||
build-type: 'Debug'
|
||||
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
|
||||
ldflags: '-fsanitize=address -fsanitize=undefined'
|
||||
|
||||
- toolset: gcc-14
|
||||
install: 'g++-14'
|
||||
container: ubuntu:24.04
|
||||
cxxstd: '23'
|
||||
build-type: 'Debug'
|
||||
cxxflags: '-DBOOST_ASIO_DISABLE_LOCAL_SOCKETS=1' # If a system had no UNIX socket support, we build correctly
|
||||
|
||||
- toolset: gcc-14
|
||||
install: 'g++-14'
|
||||
container: ubuntu:24.04
|
||||
cxxstd: '23'
|
||||
build-type: 'Debug'
|
||||
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
|
||||
ldflags: '-fsanitize=address -fsanitize=undefined'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CXXFLAGS: -g -O0 ${{matrix.cxxflags}} -Wall -Wextra
|
||||
CXXFLAGS: ${{matrix.cxxflags}} -Wall -Wextra
|
||||
LDFLAGS: ${{matrix.ldflags}}
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: 4
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install CMake
|
||||
run: sudo apt-get -y install cmake
|
||||
- name: Install protobuf
|
||||
run: sudo apt-get -y install protobuf-compiler
|
||||
- name: Install compiler
|
||||
run: sudo apt-get install -y ${{ matrix.install }}
|
||||
- name: Install Redis
|
||||
run: sudo apt-get install -y redis-server
|
||||
- name: Install boost
|
||||
uses: MarkusJx/install-boost@v2.4.1
|
||||
id: install-boost
|
||||
with:
|
||||
boost_version: 1.81.0
|
||||
platform_version: 22.04
|
||||
- name: Run CMake
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up the required containers
|
||||
run: |
|
||||
BOOST_ROOT=${{steps.install-boost.outputs.BOOST_ROOT}} cmake -DCMAKE_CXX_COMPILER="${{matrix.compiler}}" -DCMAKE_CXX_FLAGS="${{env.CXXFLAGS}}" -DCMAKE_EXE_LINKER_FLAGS="${{env.LDFLAGS}}"
|
||||
- name: Build
|
||||
run: make
|
||||
- name: Check
|
||||
run: ctest --output-on-failure
|
||||
IMAGE=${{ matrix.container }} docker compose -f tools/docker-compose.yml up -d --wait || (docker compose logs; exit 1)
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
docker exec builder apt-get update
|
||||
docker exec builder apt-get -y --no-install-recommends install \
|
||||
git \
|
||||
g++ \
|
||||
libssl-dev \
|
||||
make \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
protobuf-compiler \
|
||||
python3 \
|
||||
${{ matrix.install }}
|
||||
|
||||
- name: Setup Boost
|
||||
run: docker exec builder /boost-redis/tools/ci.py setup-boost --source-dir=/boost-redis
|
||||
|
||||
- name: Build a Boost distribution using B2
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py build-b2-distro \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Build a Boost distribution using CMake
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py build-cmake-distro \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Build the project tests
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py build-cmake-standalone-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Run the project tests
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py run-cmake-standalone-tests \
|
||||
--build-type ${{ matrix.build-type }}
|
||||
|
||||
- name: Run add_subdirectory tests
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py run-cmake-add-subdirectory-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Run find_package tests with the built cmake distribution
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py run-cmake-find-package-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
- name: Run find_package tests with the built b2 distribution
|
||||
run: |
|
||||
docker exec builder /boost-redis/tools/ci.py run-cmake-b2-find-package-tests \
|
||||
--build-type ${{ matrix.build-type }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--toolset ${{ matrix.toolset }}
|
||||
|
||||
posix-b2:
|
||||
name: "B2 ${{ matrix.toolset }}"
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- toolset: gcc-11
|
||||
install: g++-11
|
||||
cxxstd: "11,17,20" # Having C++11 shouldn't break the build
|
||||
os: ubuntu-latest
|
||||
container: ubuntu:22.04
|
||||
- toolset: clang-14
|
||||
install: clang-14
|
||||
os: ubuntu-latest
|
||||
container: ubuntu:22.04
|
||||
cxxstd: "17,20"
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{matrix.container}}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup container environment
|
||||
if: matrix.container
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get -y install sudo python3 git g++ libssl-dev
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install python3 ${{ matrix.install }}
|
||||
|
||||
- name: Setup Boost
|
||||
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
|
||||
|
||||
- name: Build and run project tests using B2
|
||||
run: |
|
||||
python3 tools/ci.py run-b2-tests \
|
||||
--toolset ${{ matrix.toolset }} \
|
||||
--cxxstd ${{ matrix.cxxstd }} \
|
||||
--variant debug,release
|
||||
|
||||
# Checks that we don't have any errors in docs
|
||||
check-docs:
|
||||
name: Check docs
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Boost
|
||||
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
cd ~/boost-root
|
||||
./b2 libs/redis/doc
|
||||
[ -f ~/boost-root/libs/redis/doc/html/index.html ]
|
||||
|
||||
47
.github/workflows/coverage.yml
vendored
47
.github/workflows/coverage.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
jobs:
|
||||
posix:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
CXX: g++-11
|
||||
CXXFLAGS: -g -O0 -std=c++20 --coverage -fkeep-inline-functions -fkeep-static-functions
|
||||
LDFLAGS: --coverage
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install CMake
|
||||
run: sudo apt-get -y install cmake
|
||||
- name: Install lcov
|
||||
run: sudo apt-get -y install lcov
|
||||
- name: Install compiler
|
||||
run: sudo apt-get -y install g++-11
|
||||
- name: Install Redis
|
||||
run: sudo apt-get -y install redis-server
|
||||
- name: Install boost
|
||||
uses: MarkusJx/install-boost@v2.4.1
|
||||
id: install-boost
|
||||
with:
|
||||
boost_version: 1.81.0
|
||||
platform_version: 22.04
|
||||
- name: Run CMake
|
||||
run: |
|
||||
BOOST_ROOT=${{steps.install-boost.outputs.BOOST_ROOT}} cmake --preset coverage .
|
||||
- name: Build
|
||||
run: cmake --build --preset coverage
|
||||
- name: Test
|
||||
run: ctest --preset coverage
|
||||
- name: Make the coverage file
|
||||
run: cmake --build --preset coverage --target coverage
|
||||
- name: Upload to codecov
|
||||
run: |
|
||||
bash <(curl -s https://codecov.io/bash) -f ./build/coverage/coverage.info || echo "Codecov did not collect coverage reports"
|
||||
|
||||
361
CMakeLists.txt
361
CMakeLists.txt
@@ -1,264 +1,141 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
#set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
|
||||
cmake_minimum_required(VERSION 3.8...3.20)
|
||||
|
||||
# determine whether it's main/root project
|
||||
# or being built under another project.
|
||||
if (NOT DEFINED BOOST_REDIS_MAIN_PROJECT)
|
||||
set(BOOST_REDIS_MAIN_PROJECT OFF)
|
||||
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
|
||||
set(BOOST_REDIS_MAIN_PROJECT ON)
|
||||
endif()
|
||||
set(BOOST_REDIS_MAIN_PROJECT OFF)
|
||||
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
|
||||
set(BOOST_REDIS_MAIN_PROJECT ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
project(
|
||||
boost_redis
|
||||
VERSION 1.4.1
|
||||
DESCRIPTION "A redis client library"
|
||||
HOMEPAGE_URL "https://boostorg.github.io/redis/"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
option(BOOST_REDIS_INSTALL "Generate install targets." ${BOOST_REDIS_MAIN_PROJECT})
|
||||
option(BOOST_REDIS_TESTS "Build tests." ${BOOST_REDIS_MAIN_PROJECT})
|
||||
option(BOOST_REDIS_EXAMPLES "Build examples." ${BOOST_REDIS_MAIN_PROJECT})
|
||||
option(BOOST_REDIS_BENCHMARKS "Build benchmarks." ${BOOST_REDIS_MAIN_PROJECT})
|
||||
option(BOOST_REDIS_DOC "Generate documentations." ${BOOST_REDIS_MAIN_PROJECT})
|
||||
project(boost_redis VERSION "${BOOST_SUPERPROJECT_VERSION}" LANGUAGES CXX)
|
||||
|
||||
# Library
|
||||
add_library(boost_redis INTERFACE)
|
||||
add_library(Boost::redis ALIAS boost_redis)
|
||||
target_include_directories(boost_redis INTERFACE
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
boost_redis
|
||||
INTERFACE
|
||||
Boost::asio
|
||||
Boost::assert
|
||||
Boost::config
|
||||
Boost::core
|
||||
Boost::mp11
|
||||
Boost::system
|
||||
Boost::utility
|
||||
)
|
||||
|
||||
target_include_directories(boost_redis INTERFACE include)
|
||||
target_compile_features(boost_redis INTERFACE cxx_std_17)
|
||||
|
||||
# Asio bases C++ feature detection on __cplusplus. Make MSVC
|
||||
# define it correctly
|
||||
if (MSVC)
|
||||
target_compile_options(boost_redis INTERFACE /Zc:__cplusplus)
|
||||
endif()
|
||||
# Dependencies
|
||||
if (BOOST_REDIS_MAIN_PROJECT)
|
||||
# TODO: Understand why we have to list all dependencies below
|
||||
# instead of
|
||||
#set(BOOST_INCLUDE_LIBRARIES redis)
|
||||
#set(BOOST_EXCLUDE_LIBRARIES redis)
|
||||
#add_subdirectory(../.. boostorg/boost EXCLUDE_FROM_ALL)
|
||||
|
||||
find_package(Boost 1.80 REQUIRED)
|
||||
set(deps
|
||||
system
|
||||
assert
|
||||
config
|
||||
throw_exception
|
||||
asio
|
||||
variant2
|
||||
mp11
|
||||
winapi
|
||||
predef
|
||||
align
|
||||
context
|
||||
core
|
||||
static_assert
|
||||
pool
|
||||
date_time
|
||||
smart_ptr
|
||||
exception
|
||||
integer
|
||||
move
|
||||
type_traits
|
||||
algorithm
|
||||
utility
|
||||
io
|
||||
lexical_cast
|
||||
numeric/conversion
|
||||
mpl
|
||||
range
|
||||
tokenizer
|
||||
tuple
|
||||
array
|
||||
bind
|
||||
concept_check
|
||||
function
|
||||
iterator
|
||||
regex
|
||||
unordered
|
||||
preprocessor
|
||||
container
|
||||
conversion
|
||||
container_hash
|
||||
detail
|
||||
optional
|
||||
function_types
|
||||
fusion
|
||||
intrusive
|
||||
describe
|
||||
typeof
|
||||
functional
|
||||
test
|
||||
json
|
||||
endian
|
||||
)
|
||||
|
||||
include_directories(${Boost_INCLUDE_DIRS})
|
||||
foreach(dep IN LISTS deps)
|
||||
add_subdirectory(../${dep} boostorg/${dep})
|
||||
endforeach()
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
include_directories(include)
|
||||
|
||||
# Common
|
||||
#=======================================================================
|
||||
|
||||
add_library(boost_redis_project_options INTERFACE)
|
||||
target_link_libraries(boost_redis_project_options INTERFACE OpenSSL::Crypto OpenSSL::SSL)
|
||||
if (MSVC)
|
||||
target_compile_options(boost_redis_project_options INTERFACE /bigobj)
|
||||
target_compile_definitions(boost_redis_project_options INTERFACE _WIN32_WINNT=0x0601)
|
||||
endif()
|
||||
|
||||
add_library(boost_redis_src STATIC examples/boost_redis.cpp)
|
||||
target_compile_features(boost_redis_src PRIVATE cxx_std_17)
|
||||
target_link_libraries(boost_redis_src PRIVATE boost_redis_project_options)
|
||||
|
||||
# Executables
|
||||
#=======================================================================
|
||||
|
||||
if (BOOST_REDIS_BENCHMARKS)
|
||||
add_library(benchmarks_options INTERFACE)
|
||||
target_link_libraries(benchmarks_options INTERFACE boost_redis_src)
|
||||
target_link_libraries(benchmarks_options INTERFACE boost_redis_project_options)
|
||||
target_compile_features(benchmarks_options INTERFACE cxx_std_20)
|
||||
|
||||
add_executable(echo_server_client benchmarks/cpp/asio/echo_server_client.cpp)
|
||||
target_link_libraries(echo_server_client PRIVATE benchmarks_options)
|
||||
|
||||
add_executable(echo_server_direct benchmarks/cpp/asio/echo_server_direct.cpp)
|
||||
target_link_libraries(echo_server_direct PRIVATE benchmarks_options)
|
||||
endif()
|
||||
|
||||
if (BOOST_REDIS_EXAMPLES)
|
||||
add_library(examples_main STATIC examples/main.cpp)
|
||||
target_compile_features(examples_main PRIVATE cxx_std_20)
|
||||
target_link_libraries(examples_main PRIVATE boost_redis_project_options)
|
||||
|
||||
macro(make_example EXAMPLE_NAME STANDARD)
|
||||
add_executable(${EXAMPLE_NAME} examples/${EXAMPLE_NAME}.cpp)
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_src)
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_project_options)
|
||||
target_compile_features(${EXAMPLE_NAME} PRIVATE cxx_std_${STANDARD})
|
||||
if (${STANDARD} STREQUAL "20")
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE examples_main)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
macro(make_testable_example EXAMPLE_NAME STANDARD)
|
||||
make_example(${EXAMPLE_NAME} ${STANDARD})
|
||||
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME})
|
||||
endmacro()
|
||||
|
||||
make_testable_example(cpp17_intro 17)
|
||||
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_example(cpp20_subscriber 20)
|
||||
make_example(cpp20_streams 20)
|
||||
make_example(cpp20_echo_server 20)
|
||||
make_example(cpp20_resolve_with_sentinel 20)
|
||||
|
||||
# We test the protobuf example only on gcc.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
find_package(Protobuf)
|
||||
if (Protobuf_FOUND)
|
||||
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS examples/person.proto)
|
||||
make_testable_example(cpp20_protobuf 20)
|
||||
target_sources(cpp20_protobuf PUBLIC ${PROTO_SRCS} ${PROTO_HDRS})
|
||||
target_link_libraries(cpp20_protobuf PRIVATE ${Protobuf_LIBRARIES})
|
||||
target_include_directories(cpp20_protobuf PUBLIC ${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
endif()
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(boost_redis
|
||||
INTERFACE
|
||||
Boost::system
|
||||
Boost::asio
|
||||
Threads::Threads
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
)
|
||||
else()
|
||||
# If we're in the superproject or called from add_subdirectory,
|
||||
# Boost dependencies should be already available.
|
||||
# If other dependencies are not found, we bail out
|
||||
find_package(Threads)
|
||||
if(NOT Threads_FOUND)
|
||||
message(STATUS "Boost.Redis has been disabled, because the required package Threads hasn't been found")
|
||||
return()
|
||||
endif()
|
||||
find_package(OpenSSL)
|
||||
if(NOT OpenSSL_FOUND)
|
||||
message(STATUS "Boost.Redis has been disabled, because the required package OpenSSL hasn't been found")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if (NOT MSVC)
|
||||
make_example(cpp20_chat_room 20)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (BOOST_REDIS_TESTS)
|
||||
enable_testing()
|
||||
|
||||
add_library(tests_common STATIC tests/common.cpp)
|
||||
target_compile_features(tests_common PRIVATE cxx_std_17)
|
||||
target_link_libraries(tests_common PRIVATE boost_redis_project_options)
|
||||
|
||||
macro(make_test TEST_NAME STANDARD)
|
||||
add_executable(${TEST_NAME} tests/${TEST_NAME}.cpp)
|
||||
target_link_libraries(${TEST_NAME} PRIVATE boost_redis_src tests_common)
|
||||
target_link_libraries(${TEST_NAME} PRIVATE boost_redis_project_options)
|
||||
target_compile_features(${TEST_NAME} PRIVATE cxx_std_${STANDARD})
|
||||
add_test(${TEST_NAME} ${TEST_NAME})
|
||||
endmacro()
|
||||
|
||||
make_test(test_conn_quit 17)
|
||||
make_test(test_conn_tls 17)
|
||||
make_test(test_low_level 17)
|
||||
make_test(test_conn_exec_retry 17)
|
||||
make_test(test_conn_exec_error 17)
|
||||
make_test(test_request 17)
|
||||
make_test(test_run 17)
|
||||
make_test(test_low_level_sync 17)
|
||||
make_test(test_low_level_sync_sans_io 17)
|
||||
make_test(test_conn_check_health 17)
|
||||
|
||||
make_test(test_conn_exec 20)
|
||||
make_test(test_conn_push 20)
|
||||
make_test(test_conn_reconnect 20)
|
||||
make_test(test_conn_exec_cancel 20)
|
||||
make_test(test_conn_exec_cancel2 20)
|
||||
make_test(test_conn_echo_stress 20)
|
||||
make_test(test_low_level_async 20)
|
||||
make_test(test_conn_run_cancel 20)
|
||||
make_test(test_issue_50 20)
|
||||
endif()
|
||||
|
||||
# Install
|
||||
#=======================================================================
|
||||
|
||||
if (BOOST_REDIS_INSTALL)
|
||||
install(TARGETS boost_redis
|
||||
EXPORT boost_redis
|
||||
PUBLIC_HEADER DESTINATION include COMPONENT Development
|
||||
)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
configure_package_config_file(
|
||||
"${PROJECT_SOURCE_DIR}/cmake/BoostRedisConfig.cmake.in"
|
||||
"${PROJECT_BINARY_DIR}/BoostRedisConfig.cmake"
|
||||
INSTALL_DESTINATION lib/cmake/boost/redis
|
||||
)
|
||||
|
||||
install(EXPORT boost_redis DESTINATION lib/cmake/boost/redis)
|
||||
install(FILES "${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.cmake"
|
||||
"${PROJECT_BINARY_DIR}/BoostRedisConfig.cmake"
|
||||
DESTINATION lib/cmake/boost/redis)
|
||||
|
||||
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
write_basic_package_version_file(
|
||||
"${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.cmake"
|
||||
COMPATIBILITY AnyNewerVersion
|
||||
)
|
||||
|
||||
include(CPack)
|
||||
endif()
|
||||
|
||||
# Doxygen
|
||||
#=======================================================================
|
||||
|
||||
if (BOOST_REDIS_DOC)
|
||||
set(DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/doc")
|
||||
configure_file(doc/Doxyfile.in doc/Doxyfile @ONLY)
|
||||
|
||||
add_custom_target(
|
||||
doc
|
||||
COMMAND doxygen "${PROJECT_BINARY_DIR}/doc/Doxyfile"
|
||||
COMMENT "Building documentation using Doxygen"
|
||||
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||
VERBATIM
|
||||
# This is generated by boostdep
|
||||
target_link_libraries(boost_redis
|
||||
INTERFACE
|
||||
Boost::asio
|
||||
Boost::assert
|
||||
Boost::core
|
||||
Boost::mp11
|
||||
Boost::system
|
||||
Boost::throw_exception
|
||||
Threads::Threads
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
)
|
||||
endif()
|
||||
|
||||
# Coverage
|
||||
#=======================================================================
|
||||
# Enable testing. If we're being called from the superproject, this has already been done
|
||||
if (BOOST_REDIS_MAIN_PROJECT)
|
||||
include(CTest)
|
||||
endif()
|
||||
|
||||
set(
|
||||
COVERAGE_TRACE_COMMAND
|
||||
lcov --capture
|
||||
-output-file "${PROJECT_BINARY_DIR}/coverage.info"
|
||||
--directory "${PROJECT_BINARY_DIR}"
|
||||
--include "${PROJECT_SOURCE_DIR}/include/*"
|
||||
)
|
||||
# Most tests require a running Redis server, so we only run them if we're the main project
|
||||
if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING)
|
||||
# Tests and common utilities
|
||||
add_subdirectory(test)
|
||||
|
||||
set(
|
||||
COVERAGE_HTML_COMMAND
|
||||
genhtml --legend -f -q
|
||||
"${PROJECT_BINARY_DIR}/coverage.info"
|
||||
--prefix "${PROJECT_SOURCE_DIR}"
|
||||
--output-directory "${PROJECT_BINARY_DIR}/coverage_html"
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
coverage
|
||||
COMMAND ${COVERAGE_TRACE_COMMAND}
|
||||
COMMAND ${COVERAGE_HTML_COMMAND}
|
||||
COMMENT "Generating coverage report"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# TODO
|
||||
#=======================================================================
|
||||
|
||||
#.PHONY: bench
|
||||
#bench:
|
||||
# pdflatex --jobname=echo-f0 benchmarks/benchmarks.tex
|
||||
# pdflatex --jobname=echo-f1 benchmarks/benchmarks.tex
|
||||
# pdftoppm {input.pdf} {output.file} -png
|
||||
# Benchmarks. Build them with tests to prevent code rotting
|
||||
add_subdirectory(benchmarks)
|
||||
|
||||
# Examples
|
||||
add_subdirectory(example)
|
||||
endif()
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"warnings": {
|
||||
"dev": true,
|
||||
"deprecated": true,
|
||||
"uninitialized": true,
|
||||
"uninitialized": false,
|
||||
"unusedCli": true,
|
||||
"systemVars": false
|
||||
},
|
||||
@@ -52,8 +52,7 @@
|
||||
"CMAKE_CXX_COMPILER": "g++-11",
|
||||
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11",
|
||||
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11/doc/"
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -69,8 +68,7 @@
|
||||
"CMAKE_CXX_COMPILER": "g++-11",
|
||||
"CMAKE_SHARED_LINKER_FLAGS": "",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-release",
|
||||
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11-release/doc/"
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-release"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,8 +84,23 @@
|
||||
"CMAKE_CXX_COMPILER": "clang++-13",
|
||||
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-13",
|
||||
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/clang++-13/doc/"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -104,8 +117,7 @@
|
||||
"CMAKE_CXX_COMPILER": "clang++-14",
|
||||
"CMAKE_SHARED_LINKER_FLAGS": "",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp17",
|
||||
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/libc++-14-cpp17/doc/"
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp17"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -122,8 +134,7 @@
|
||||
"CMAKE_CXX_COMPILER": "clang++-14",
|
||||
"CMAKE_SHARED_LINKER_FLAGS": "",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp20",
|
||||
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/libc++-14-cpp20/doc/"
|
||||
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp20"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -143,6 +154,7 @@
|
||||
{ "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" }
|
||||
@@ -158,6 +170,7 @@
|
||||
{ "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"] }
|
||||
|
||||
20
benchmarks/CMakeLists.txt
Normal file
20
benchmarks/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
add_library(benchmarks_options INTERFACE)
|
||||
target_link_libraries(benchmarks_options INTERFACE boost_redis_src)
|
||||
target_link_libraries(benchmarks_options INTERFACE boost_redis_project_options)
|
||||
target_compile_features(benchmarks_options INTERFACE cxx_std_20)
|
||||
|
||||
add_executable(echo_server_client cpp/asio/echo_server_client.cpp)
|
||||
target_link_libraries(echo_server_client PRIVATE benchmarks_options)
|
||||
|
||||
add_executable(echo_server_direct cpp/asio/echo_server_direct.cpp)
|
||||
target_link_libraries(echo_server_direct PRIVATE benchmarks_options)
|
||||
|
||||
# TODO
|
||||
#=======================================================================
|
||||
|
||||
#.PHONY: bench
|
||||
#bench:
|
||||
# pdflatex --jobname=echo-f0 benchmarks/benchmarks.tex
|
||||
# pdflatex --jobname=echo-f1 benchmarks/benchmarks.tex
|
||||
# pdftoppm {input.pdf} {output.file} -png
|
||||
@@ -4,8 +4,9 @@
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
@@ -26,9 +27,9 @@ auto example(boost::asio::ip::tcp::endpoint ep, std::string msg, int n) -> net::
|
||||
auto dbuffer = net::dynamic_buffer(buffer);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
co_await net::async_write(socket, net::buffer(msg));
|
||||
auto n = co_await net::async_read_until(socket, dbuffer, "\n");
|
||||
auto bytes_read = co_await net::async_read_until(socket, dbuffer, "\n");
|
||||
//std::printf("> %s", buffer.data());
|
||||
dbuffer.consume(n);
|
||||
dbuffer.consume(bytes_read);
|
||||
}
|
||||
|
||||
//std::printf("Ok: %s", msg.data());
|
||||
@@ -62,6 +63,10 @@ int main(int argc, char* argv[])
|
||||
std::cerr << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
auto main() -> int {std::cout << "Requires coroutine support." << std::endl; return 1;}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
auto main() -> int
|
||||
{
|
||||
std::cout << "Requires coroutine support." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <boost/asio.hpp>
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
@@ -27,37 +28,41 @@ constexpr net::use_awaitable_t<executor_type> use_awaitable;
|
||||
|
||||
awaitable_type echo(tcp_socket socket)
|
||||
{
|
||||
try {
|
||||
char data[1024];
|
||||
for (;;) {
|
||||
std::size_t n = co_await socket.async_read_some(net::buffer(data), use_awaitable);
|
||||
co_await async_write(socket, net::buffer(data, n), use_awaitable);
|
||||
}
|
||||
} catch (std::exception const&) {
|
||||
//std::printf("echo Exception: %s\n", e.what());
|
||||
}
|
||||
try {
|
||||
char data[1024];
|
||||
for (;;) {
|
||||
std::size_t n = co_await socket.async_read_some(net::buffer(data), use_awaitable);
|
||||
co_await async_write(socket, net::buffer(data, n), use_awaitable);
|
||||
}
|
||||
} catch (std::exception const&) {
|
||||
//std::printf("echo Exception: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
awaitable_type listener()
|
||||
{
|
||||
auto ex = co_await this_coro::executor;
|
||||
tcp_acceptor acceptor(ex, {tcp::v4(), 55555});
|
||||
for (;;) {
|
||||
tcp_socket socket = co_await acceptor.async_accept(use_awaitable);
|
||||
co_spawn(ex, echo(std::move(socket)), detached);
|
||||
}
|
||||
auto ex = co_await this_coro::executor;
|
||||
tcp_acceptor acceptor(ex, {tcp::v4(), 55555});
|
||||
for (;;) {
|
||||
tcp_socket socket = co_await acceptor.async_accept(use_awaitable);
|
||||
co_spawn(ex, echo(std::move(socket)), detached);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
try {
|
||||
net::io_context io_context{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO};
|
||||
co_spawn(io_context, listener(), detached);
|
||||
io_context.run();
|
||||
} catch (std::exception const& e) {
|
||||
std::printf("Exception: %s\n", e.what());
|
||||
}
|
||||
try {
|
||||
net::io_context io_context{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO};
|
||||
co_spawn(io_context, listener(), detached);
|
||||
io_context.run();
|
||||
} catch (std::exception const& e) {
|
||||
std::printf("Exception: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
auto main() -> int {std::cout << "Requires coroutine support." << std::endl; return 1;}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
auto main() -> int
|
||||
{
|
||||
std::cout << "Requires coroutine support." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
28
build.jam
Normal file
28
build.jam
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright René Ferdinand Rivera Morell 2024
|
||||
# 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)
|
||||
|
||||
require-b2 5.2 ;
|
||||
|
||||
constant boost_dependencies :
|
||||
/boost/asio//boost_asio
|
||||
/boost/assert//boost_assert
|
||||
/boost/core//boost_core
|
||||
/boost/mp11//boost_mp11
|
||||
/boost/system//boost_system
|
||||
/boost/throw_exception//boost_throw_exception ;
|
||||
|
||||
project /boost/redis
|
||||
: common-requirements
|
||||
<include>include
|
||||
;
|
||||
|
||||
explicit
|
||||
[ alias boost_redis : : : : <library>$(boost_dependencies) ]
|
||||
[ alias all : boost_redis test ]
|
||||
;
|
||||
|
||||
call-if : boost-library redis
|
||||
;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/Aedis.cmake")
|
||||
check_required_components("@PROJECT_NAME@")
|
||||
26
doc/CMakeLists.txt
Normal file
26
doc/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
# Root CMakeLists.txt so MrDocs knows how to build our code
|
||||
cmake_minimum_required(VERSION 3.8...3.22)
|
||||
|
||||
# Project
|
||||
project(boost_redis_mrdocs LANGUAGES CXX)
|
||||
|
||||
# MrDocs forces CMAKE_EXPORT_COMPILE_COMMANDS=ON, incorrectly
|
||||
# causing all targets to be dumped to the compilation database.
|
||||
# Disable this setting so we can set EXPORT_COMPILE_COMMANDS
|
||||
# only to our target of interest
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS OFF)
|
||||
|
||||
# Add Boost
|
||||
add_subdirectory($ENV{BOOST_SRC_DIR} deps/boost)
|
||||
|
||||
# Add the target for mrdocs to analyze
|
||||
add_executable(mrdocs mrdocs.cpp)
|
||||
target_link_libraries(mrdocs PRIVATE Boost::redis)
|
||||
set_target_properties(mrdocs PROPERTIES EXPORT_COMPILE_COMMANDS ON)
|
||||
2690
doc/Doxyfile.in
2690
doc/Doxyfile.in
File diff suppressed because it is too large
Load Diff
@@ -1,226 +0,0 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.9.1 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title="Contents"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="modules" visible="no" title="Reference" intro=""/>
|
||||
<tab type="namespaces" visible="no" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="interfaces" visible="no" title="">
|
||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="classes" visible="no" title="">
|
||||
<tab type="classlist" visible="no" title="" intro=""/>
|
||||
<tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="structs" visible="no" title="">
|
||||
<tab type="structlist" visible="no" title="" intro=""/>
|
||||
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
</tab>
|
||||
<tab type="exceptions" visible="no" title="">
|
||||
<tab type="exceptionlist" visible="no" title="" intro=""/>
|
||||
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="files" visible="no" title="">
|
||||
<tab type="filelist" visible="no" title="" intro=""/>
|
||||
<tab type="globals" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<inheritancegraph visible="$CLASS_GRAPH"/>
|
||||
<collaborationgraph visible="$COLLABORATION_GRAPH"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<publicslots title=""/>
|
||||
<signals title=""/>
|
||||
<publicmethods title=""/>
|
||||
<publicstaticmethods title=""/>
|
||||
<publicattributes title=""/>
|
||||
<publicstaticattributes title=""/>
|
||||
<protectedtypes title=""/>
|
||||
<protectedslots title=""/>
|
||||
<protectedmethods title=""/>
|
||||
<protectedstaticmethods title=""/>
|
||||
<protectedattributes title=""/>
|
||||
<protectedstaticattributes title=""/>
|
||||
<packagetypes title=""/>
|
||||
<packagemethods title=""/>
|
||||
<packagestaticmethods title=""/>
|
||||
<packageattributes title=""/>
|
||||
<packagestaticattributes title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
<privatetypes title=""/>
|
||||
<privateslots title=""/>
|
||||
<privatemethods title=""/>
|
||||
<privatestaticmethods title=""/>
|
||||
<privateattributes title=""/>
|
||||
<privatestaticattributes title=""/>
|
||||
<friends title=""/>
|
||||
<related title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<constructors title=""/>
|
||||
<functions title=""/>
|
||||
<related title=""/>
|
||||
<variables title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="$INCLUDE_GRAPH"/>
|
||||
<includedbygraph visible="$INCLUDED_BY_GRAPH"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<groupgraph visible="$GROUP_GRAPHS"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
24
doc/Jamfile
Normal file
24
doc/Jamfile
Normal file
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
# Adapted from Boost.Unordered
|
||||
make html/index.html : build_antora.sh : @run-script ;
|
||||
|
||||
# Runs the Antora script
|
||||
actions run-script
|
||||
{
|
||||
bash -x $(>)
|
||||
}
|
||||
|
||||
# These are used to inform the build system of the
|
||||
# means to build the integrated and stand-alone docs.
|
||||
|
||||
alias boostdoc ;
|
||||
explicit boostdoc ;
|
||||
|
||||
alias boostrelease : html/index.html ;
|
||||
explicit boostrelease ;
|
||||
15
doc/antora.yml
Normal file
15
doc/antora.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
name: redis
|
||||
title: Boost.Redis
|
||||
version: ~
|
||||
nav:
|
||||
- modules/ROOT/nav.adoc
|
||||
ext:
|
||||
cpp-reference:
|
||||
config: doc/mrdocs.yml
|
||||
19
doc/build_antora.sh
Executable file
19
doc/build_antora.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Required by our CMake.
|
||||
# Prevents Antora from cloning Boost again
|
||||
export BOOST_SRC_DIR=$(realpath $SCRIPT_DIR/../../..)
|
||||
|
||||
npm ci
|
||||
npx antora --log-format=pretty redis-playbook.yml
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
html {
|
||||
/* side nav width. MUST be = `TREEVIEW_WIDTH`.
|
||||
* Make sure it is wide enough to contain the page title (logo + title + version)
|
||||
*/
|
||||
--side-nav-fixed-width: 335px;
|
||||
--menu-display: none;
|
||||
|
||||
--top-height: 120px;
|
||||
--toc-sticky-top: -25px;
|
||||
--toc-max-height: calc(100vh - 2 * var(--spacing-medium) - 25px);
|
||||
}
|
||||
|
||||
#projectname {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
html {
|
||||
--searchbar-background: var(--page-background-color);
|
||||
}
|
||||
|
||||
#side-nav {
|
||||
min-width: var(--side-nav-fixed-width);
|
||||
max-width: var(--side-nav-fixed-width);
|
||||
top: var(--top-height);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#nav-tree, #side-nav {
|
||||
height: calc(100vh - var(--top-height)) !important;
|
||||
}
|
||||
|
||||
#nav-tree {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#top {
|
||||
display: block;
|
||||
border-bottom: none;
|
||||
height: var(--top-height);
|
||||
margin-bottom: calc(0px - var(--top-height));
|
||||
max-width: var(--side-nav-fixed-width);
|
||||
overflow: hidden;
|
||||
background: var(--side-nav-background);
|
||||
}
|
||||
#main-nav {
|
||||
float: left;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
cursor: default;
|
||||
width: 1px !important;
|
||||
box-shadow: 0 calc(-2 * var(--top-height)) 0 0 var(--separator-color);
|
||||
}
|
||||
|
||||
#nav-path {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: var(--side-nav-fixed-width);
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#doc-content {
|
||||
height: calc(100vh - 31px) !important;
|
||||
padding-bottom: calc(3 * var(--spacing-large));
|
||||
padding-top: calc(var(--top-height) - 80px);
|
||||
box-sizing: border-box;
|
||||
margin-left: var(--side-nav-fixed-width) !important;
|
||||
}
|
||||
|
||||
#MSearchBox {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)));
|
||||
}
|
||||
|
||||
#MSearchField {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 65px);
|
||||
}
|
||||
|
||||
#MSearchResultsWindow {
|
||||
left: var(--spacing-medium) !important;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
10
doc/modules/ROOT/nav.adoc
Normal file
10
doc/modules/ROOT/nav.adoc
Normal file
@@ -0,0 +1,10 @@
|
||||
* xref:index.adoc[Introduction]
|
||||
* xref:requests_responses.adoc[]
|
||||
* xref:serialization.adoc[]
|
||||
* xref:logging.adoc[]
|
||||
* xref:benchmarks.adoc[]
|
||||
* xref:comparison.adoc[]
|
||||
* xref:examples.adoc[]
|
||||
* xref:reference.adoc[Reference]
|
||||
* xref:acknowledgements.adoc[]
|
||||
* xref:changelog.adoc[]
|
||||
37
doc/modules/ROOT/pages/acknowledgements.adoc
Normal file
37
doc/modules/ROOT/pages/acknowledgements.adoc
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Acknowledgements
|
||||
|
||||
Acknowledgement to people that helped shape Boost.Redis
|
||||
|
||||
* Richard Hodges (https://github.com/madmongo1[madmongo1]): For very helpful support with Asio, the design of asynchronous programs, etc.
|
||||
* Vinícius dos Santos Oliveira (https://github.com/vinipsmaker[vinipsmaker]): For useful discussion about how Boost.Redis consumes buffers in the read operation.
|
||||
* Petr Dannhofer (https://github.com/Eddie-cz[Eddie-cz]): For helping me understand how the `AUTH` and `HELLO` command can influence each other.
|
||||
* Mohammad Nejati (https://github.com/ashtum[ashtum]): For pointing out scenarios where calls to `async_exec` should fail when the connection is lost.
|
||||
* Klemens Morgenstern (https://github.com/klemens-morgenstern[klemens-morgenstern]): For useful discussion about timeouts, cancellation, synchronous interfaces and general help with Asio.
|
||||
* Vinnie Falco (https://github.com/vinniefalco[vinniefalco]): For general suggestions about how to improve the code and the documentation.
|
||||
* Bram Veldhoen (https://github.com/bveldhoen[bveldhoen]): For contributing a Redis-streams example.
|
||||
|
||||
Also many thanks to all individuals that participated in the Boost
|
||||
review
|
||||
|
||||
* Zach Laine: https://lists.boost.org/Archives/boost/2023/01/253883.php
|
||||
* Vinnie Falco: https://lists.boost.org/Archives/boost/2023/01/253886.php
|
||||
* Christian Mazakas: https://lists.boost.org/Archives/boost/2023/01/253900.php
|
||||
* Ruben Perez: https://lists.boost.org/Archives/boost/2023/01/253915.php
|
||||
* Dmitry Arkhipov: https://lists.boost.org/Archives/boost/2023/01/253925.php
|
||||
* Alan de Freitas: https://lists.boost.org/Archives/boost/2023/01/253927.php
|
||||
* Mohammad Nejati: https://lists.boost.org/Archives/boost/2023/01/253929.php
|
||||
* Sam Hartsfield: https://lists.boost.org/Archives/boost/2023/01/253931.php
|
||||
* Miguel Portilla: https://lists.boost.org/Archives/boost/2023/01/253935.php
|
||||
* Robert A.H. Leahy: https://lists.boost.org/Archives/boost/2023/01/253928.php
|
||||
|
||||
The Reviews can be found at:
|
||||
https://lists.boost.org/Archives/boost/2023/01/date.php. The thread
|
||||
with the ACCEPT from the review manager can be found here:
|
||||
https://lists.boost.org/Archives/boost/2023/01/253944.php.
|
||||
95
doc/modules/ROOT/pages/benchmarks.adoc
Normal file
95
doc/modules/ROOT/pages/benchmarks.adoc
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Echo server benchmark
|
||||
|
||||
This document benchmarks the performance of TCP echo servers I
|
||||
implemented in different languages using different Redis clients. The
|
||||
main motivations for choosing an echo server are
|
||||
|
||||
* Simple to implement and does not require expertise level in most languages.
|
||||
* I/O bound: Echo servers have very low CPU consumption in general
|
||||
and therefore are excellent to measure how a program handles concurrent requests.
|
||||
* It simulates very well a typical backend in regard to concurrency.
|
||||
|
||||
I also imposed some constraints on the implementations
|
||||
|
||||
* It should be simple enough and not require writing too much code.
|
||||
* Favor the use standard idioms and avoid optimizations that require expert level.
|
||||
* Avoid the use of complex things like connection and thread pool.
|
||||
|
||||
To reproduce these results run one of the echo-server programs in one
|
||||
terminal and the
|
||||
https://github.com/boostorg/redis/blob/42880e788bec6020dd018194075a211ad9f339e8/benchmarks/cpp/asio/echo_server_client.cpp[echo-server-client] in another.
|
||||
|
||||
== Without Redis
|
||||
|
||||
First I tested a pure TCP echo server, i.e. one that sends the messages
|
||||
directly to the client without interacting with Redis. The result can
|
||||
be seen below
|
||||
|
||||
image::https://boostorg.github.io/redis/tcp-echo-direct.png[]
|
||||
|
||||
The tests were performed with a 1000 concurrent TCP connections on the
|
||||
localhost where latency is 0.07ms on average on my machine. On higher
|
||||
latency networks the difference among libraries is expected to
|
||||
decrease.
|
||||
|
||||
* I expected Libuv to have similar performance to Asio and Tokio.
|
||||
* I did expect nodejs to come a little behind given it is is
|
||||
javascript code. Otherwise I did expect it to have similar
|
||||
performance to libuv since it is the framework behind it.
|
||||
* Go did surprise me: faster than nodejs and libuv!
|
||||
|
||||
The code used in the benchmarks can be found at
|
||||
|
||||
* https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/cpp/asio/echo_server_direct.cpp[Asio]: A variation of https://github.com/chriskohlhoff/asio/blob/4915cfd8a1653c157a1480162ae5601318553eb8/asio/src/examples/cpp20/coroutines/echo_server.cpp[this Asio example].
|
||||
* https://github.com/boostorg/redis/tree/835a1decf477b09317f391eddd0727213cdbe12b/benchmarks/c/libuv[Libuv]: Taken from https://github.com/libuv/libuv/blob/06948c6ee502862524f233af4e2c3e4ca876f5f6/docs/code/tcp-echo-server/main.c[this Libuv example].
|
||||
* https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/rust/echo_server_direct[Tokio]: Taken from https://docs.rs/tokio/latest/tokio/[here].
|
||||
* https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_direct[Nodejs]
|
||||
* https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_direct.go[Go]
|
||||
|
||||
== With Redis
|
||||
|
||||
This is similar to the echo server described above but messages are
|
||||
echoed by Redis and not by the echo-server itself, which acts
|
||||
as a proxy between the client and the Redis server. The results
|
||||
can be seen below
|
||||
|
||||
image::https://boostorg.github.io/redis/tcp-echo-over-redis.png[]
|
||||
|
||||
The tests were performed on a network where latency is 35ms on
|
||||
average, otherwise it uses the same number of TCP connections
|
||||
as the previous example.
|
||||
|
||||
As the reader can see, the Libuv and the Rust test are not depicted
|
||||
in the graph, the reasons are
|
||||
|
||||
* https://github.com/redis-rs/redis-rs[redis-rs]: This client
|
||||
comes so far behind that it can't even be represented together
|
||||
with the other benchmarks without making them look insignificant.
|
||||
I don't know for sure why it is so slow, I suppose it has
|
||||
something to do with its lack of automatic
|
||||
https://redis.io/docs/manual/pipelining/[pipelining] support.
|
||||
In fact, the more TCP connections I launch the worse its
|
||||
performance gets.
|
||||
|
||||
* Libuv: I left it out because it would require me writing to much
|
||||
c code. More specifically, I would have to use hiredis and
|
||||
implement support for pipelines manually.
|
||||
|
||||
The code used in the benchmarks can be found at
|
||||
|
||||
* https://github.com/boostorg/redis[Boost.Redis]: https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/examples/echo_server.cpp[code]
|
||||
* https://github.com/redis/node-redis[node-redis]: https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_over_redis[code]
|
||||
* https://github.com/go-redis/redis[go-redis]: https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_over_redis.go[code]
|
||||
|
||||
|
||||
== Conclusion
|
||||
|
||||
Redis clients have to support automatic pipelining to have competitive performance. For updates to this document follow https://github.com/boostorg/redis[].
|
||||
|
||||
397
doc/modules/ROOT/pages/changelog.adoc
Normal file
397
doc/modules/ROOT/pages/changelog.adoc
Normal file
@@ -0,0 +1,397 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Changelog
|
||||
|
||||
== Boost 1.88
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/233[233])
|
||||
To deal with keys that might not exits in the Redis server, the
|
||||
library supports `std::optional`, for example
|
||||
`response<std::optional<std::vector<std::string>>>`. In some cases
|
||||
however, such as the https://redis.io/docs/latest/commands/mget/[MGET] command,
|
||||
each element in the vector might be non exiting, now it is possible
|
||||
to specify a response as `response<std::optional<std::vector<std::optional<std::string>>>>`.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/225[225])
|
||||
Use `deferred` as the connection default completion token.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/128[128])
|
||||
Adds a new `async_exec` overload that allows passing response
|
||||
adapters. This makes it possible to receive Redis responses directly
|
||||
in custom data structures thereby avoiding unnecessary data copying.
|
||||
Thanks to Ruben Perez (@anarthal) for implementing this feature.
|
||||
|
||||
* There are also other multiple small improvements in this release,
|
||||
users can refer to the git history for more details.
|
||||
|
||||
== Boost 1.87
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/205[205])
|
||||
Improves reaction time to disconnection by using `wait_for_one_error`
|
||||
instead of `wait_for_all`. The function `connection::async_run` was
|
||||
also changed to return EOF to the user when that error is received
|
||||
from the server. That is a breaking change.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/210[210])
|
||||
Fixes the adapter of empty nested responses.
|
||||
|
||||
* (Issues https://github.com/boostorg/redis/issues/211[211] and https://github.com/boostorg/redis/issues/212[212])
|
||||
Fixes the reconnect loop that would hang under certain conditions,
|
||||
see the linked issues for more details.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/219[219])
|
||||
Changes the default log level from `disabled` to `debug`.
|
||||
|
||||
== Boost 1.85
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/170[170])
|
||||
Under load and on low-latency networks it is possible to start
|
||||
receiving responses before the write operation completed and while
|
||||
the request is still marked as staged and not written. This messes
|
||||
up with the heuristics that classifies responses as unsolicited or
|
||||
not.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/168[168]).
|
||||
Provides a way of passing a custom SSL context to the connection.
|
||||
The design here differs from that of Boost.Beast and Boost.MySql
|
||||
since in Boost.Redis the connection owns the context instead of only
|
||||
storing a reference to a user provided one. This is ok so because
|
||||
apps need only one connection for their entire application, which
|
||||
makes the overhead of one ssl-context per connection negligible.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/181[181]).
|
||||
See a detailed description of this bug in
|
||||
https://github.com/boostorg/redis/issues/181#issuecomment-1913346983[this comment].
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/182[182]).
|
||||
Sets `"default"` as the default value of `config::username`. This
|
||||
makes it simpler to use the `requirepass` configuration in Redis.
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/189[189]).
|
||||
Fixes narrowing conversion by using `std::size_t` instead of
|
||||
`std::uint64_t` for the sizes of bulks and aggregates. The code
|
||||
relies now on `std::from_chars` returning an error if a value
|
||||
greater than 32 is received on platforms on which the size
|
||||
of `std::size_t` is 32.
|
||||
|
||||
|
||||
== Boost 1.84 (First release in Boost)
|
||||
|
||||
* Deprecates the `async_receive` overload that takes a response. Users
|
||||
should now first call `set_receive_response` to avoid constantly and
|
||||
unnecessarily setting the same response.
|
||||
|
||||
* Uses `std::function` to type erase the response adapter. This change
|
||||
should not influence users in any way but allowed important
|
||||
simplification in the connections internals. This resulted in
|
||||
massive performance improvement.
|
||||
|
||||
* The connection has a new member `get_usage()` that returns the
|
||||
connection usage information, such as number of bytes written,
|
||||
received etc.
|
||||
|
||||
* There are massive performance improvements in the consuming of
|
||||
server pushes which are now communicated with an `asio::channel` and
|
||||
therefore can be buffered which avoids blocking the socket read-loop.
|
||||
Batch reads are also supported by means of `channel.try_send` and
|
||||
buffered messages can be consumed synchronously with
|
||||
`connection::receive`. The function `boost::redis::cancel_one` has
|
||||
been added to simplify processing multiple server pushes contained
|
||||
in the same `generic_response`. *IMPORTANT*: These changes may
|
||||
result in more than one push in the response when
|
||||
`connection::async_receive` resumes. The user must therefore be
|
||||
careful when calling `resp.clear()`: either ensure that all message
|
||||
have been processed or just use `consume_one`.
|
||||
|
||||
== v1.4.2 (incorporates changes to conform the boost review and more)
|
||||
|
||||
* Adds `boost::redis::config::database_index` to make it possible to
|
||||
choose a database before starting running commands e.g. after an
|
||||
automatic reconnection.
|
||||
|
||||
* Massive performance improvement. One of my tests went from
|
||||
140k req/s to 390k/s. This was possible after a parser
|
||||
simplification that reduced the number of reschedules and buffer
|
||||
rotations.
|
||||
|
||||
* Adds Redis stream example.
|
||||
|
||||
* Renames the project to Boost.Redis and moves the code into namespace
|
||||
`boost::redis`.
|
||||
|
||||
* As pointed out in the reviews the `to_bulk` and `from_bulk` names were too
|
||||
generic for ADL customization points. They gained the prefix `boost_redis_`.
|
||||
|
||||
* Moves `boost::redis::resp3::request` to `boost::redis::request`.
|
||||
|
||||
* Adds new typedef `boost::redis::response` that should be used instead of
|
||||
`std::tuple`.
|
||||
|
||||
* Adds new typedef `boost::redis::generic_response` that should be used instead
|
||||
of `std::vector<resp3::node<std::string>>`.
|
||||
|
||||
* Renames `redis::ignore` to `redis::ignore_t`.
|
||||
|
||||
* Changes `async_exec` to receive a `redis::response` instead of an adapter,
|
||||
namely, instead of passing `adapt(resp)` users should pass `resp` directly.
|
||||
|
||||
* Introduces `boost::redis::adapter::result` to store responses to commands
|
||||
including possible resp3 errors without losing the error diagnostic part. To
|
||||
access values now use `std::get<N>(resp).value()` instead of
|
||||
`std::get<N>(resp)`.
|
||||
|
||||
* Implements full-duplex communication. Before these changes the connection
|
||||
would wait for a response to arrive before sending the next one. Now requests
|
||||
are continuously coalesced and written to the socket. `request::coalesce`
|
||||
became unnecessary and was removed. I could measure significative performance
|
||||
gains with these changes.
|
||||
|
||||
* Improves serialization examples using Boost.Describe to serialize to JSON and protobuf. See
|
||||
cpp20_json.cpp and cpp20_protobuf.cpp for more details.
|
||||
|
||||
* Upgrades to Boost 1.81.0.
|
||||
|
||||
* Fixes build with pass:[libc++].
|
||||
|
||||
* Adds high-level functionality to the connection classes. For
|
||||
example, `boost::redis::connection::async_run` will automatically
|
||||
resolve, connect, reconnect and perform health checks.
|
||||
|
||||
== v1.4.0-1
|
||||
|
||||
* Renames `retry_on_connection_lost` to `cancel_if_unresponded`. (v1.4.1)
|
||||
* Removes dependency on Boost.Hana, `boost::string_view`, Boost.Variant2 and Boost.Spirit.
|
||||
* Fixes build and setup CI on windows.
|
||||
|
||||
== v1.3.0-1
|
||||
|
||||
* Upgrades to Boost 1.80.0
|
||||
|
||||
* Removes automatic sending of the `HELLO` command. This can't be
|
||||
implemented properly without bloating the connection class. It is
|
||||
now a user responsibility to send HELLO. Requests that contain it have
|
||||
priority over other requests and will be moved to the front of the
|
||||
queue, see `aedis::request::config`
|
||||
|
||||
* Automatic name resolving and connecting have been removed from
|
||||
`aedis::connection::async_run`. Users have to do this step manually
|
||||
now. The reason for this change is that having them built-in doesn't
|
||||
offer enough flexibility that is need for boost users.
|
||||
|
||||
* Removes healthy checks and idle timeout. This functionality must now
|
||||
be implemented by users, see the examples. This is
|
||||
part of making Aedis useful to a larger audience and suitable for
|
||||
the Boost review process.
|
||||
|
||||
* The `aedis::connection` is now using a typeddef to a
|
||||
`net::ip::tcp::socket` and `aedis::ssl::connection` to
|
||||
`net::ssl::stream<net::ip::tcp::socket>`. Users that need to use
|
||||
other stream type must now specialize `aedis::basic_connection`.
|
||||
|
||||
* Adds a low level example of async code.
|
||||
|
||||
== v1.2.0
|
||||
|
||||
* `aedis::adapt` supports now tuples created with `std::tie`.
|
||||
`aedis::ignore` is now an alias to the type of `std::ignore`.
|
||||
|
||||
* Provides allocator support for the internal queue used in the
|
||||
`aedis::connection` class.
|
||||
|
||||
* Changes the behaviour of `async_run` to complete with success if
|
||||
asio::error::eof is received. This makes it easier to write
|
||||
composed operations with awaitable operators.
|
||||
|
||||
* Adds allocator support in the `aedis::request` (a
|
||||
contribution from Klemens Morgenstern).
|
||||
|
||||
* Renames `aedis::request::push_range2` to `push_range`. The
|
||||
suffix 2 was used for disambiguation. Klemens fixed it with SFINAE.
|
||||
|
||||
* Renames `fail_on_connection_lost` to
|
||||
`aedis::request::config::cancel_on_connection_lost`. Now, it will
|
||||
only cause connections to be canceled when `async_run` completes.
|
||||
|
||||
* Introduces `aedis::request::config::cancel_if_not_connected` which will
|
||||
cause a request to be canceled if `async_exec` is called before a
|
||||
connection has been established.
|
||||
|
||||
* Introduces new request flag `aedis::request::config::retry` that if
|
||||
set to true will cause the request to not be canceled when it was
|
||||
sent to Redis but remained unresponded after `async_run` completed.
|
||||
It provides a way to avoid executing commands twice.
|
||||
|
||||
* Removes the `aedis::connection::async_run` overload that takes
|
||||
request and adapter as parameters.
|
||||
|
||||
* Changes the way `aedis::adapt()` behaves with
|
||||
`std::vector<aedis::resp3::node<T>>`. Receiving RESP3 simple errors,
|
||||
blob errors or null won't causes an error but will be treated as
|
||||
normal response. It is the user responsibility to check the content
|
||||
in the vector.
|
||||
|
||||
* Fixes a bug in `connection::cancel(operation::exec)`. Now this
|
||||
call will only cancel non-written requests.
|
||||
|
||||
* Implements per-operation implicit cancellation support for
|
||||
`aedis::connection::async_exec`. The following call will `co_await (conn.async_exec(...) || timer.async_wait(...))`
|
||||
will cancel the request as long as it has not been written.
|
||||
|
||||
* Changes `aedis::connection::async_run` completion signature to
|
||||
`f(error_code)`. This is how is was in the past, the second
|
||||
parameter was not helpful.
|
||||
|
||||
* Renames `operation::receive_push` to `aedis::operation::receive`.
|
||||
|
||||
== v1.1.0-1
|
||||
|
||||
* Removes `coalesce_requests` from the `aedis::connection::config`, it
|
||||
became a request property now, see `aedis::request::config::coalesce`.
|
||||
|
||||
* Removes `max_read_size` from the `aedis::connection::config`. The maximum
|
||||
read size can be specified now as a parameter of the
|
||||
`aedis::adapt()` function.
|
||||
|
||||
* Removes `aedis::sync` class, see intro_sync.cpp for how to perform
|
||||
synchronous and thread safe calls. This is possible in Boost. 1.80
|
||||
only as it requires `boost::asio::deferred`.
|
||||
|
||||
* Moves from `boost::optional` to `std::optional`. This is part of
|
||||
moving to pass:[C++17].
|
||||
|
||||
* Changes the behaviour of the second `aedis::connection::async_run` overload
|
||||
so that it always returns an error when the connection is lost.
|
||||
|
||||
* Adds TLS support, see intro_tls.cpp.
|
||||
|
||||
* Adds an example that shows how to resolve addresses over sentinels,
|
||||
see subscriber_sentinel.cpp.
|
||||
|
||||
* Adds a `aedis::connection::timeouts::resp3_handshake_timeout`. This is
|
||||
timeout used to send the `HELLO` command.
|
||||
|
||||
* Adds `aedis::endpoint` where in addition to host and port, users can
|
||||
optionally provide username, password and the expected server role
|
||||
(see `aedis::error::unexpected_server_role`).
|
||||
|
||||
* `aedis::connection::async_run` checks whether the server role received in
|
||||
the hello command is equal to the expected server role specified in
|
||||
`aedis::endpoint`. To skip this check let the role variable empty.
|
||||
|
||||
* Removes reconnect functionality from `aedis::connection`. It
|
||||
is possible in simple reconnection strategies but bloats the class
|
||||
in more complex scenarios, for example, with sentinel,
|
||||
authentication and TLS. This is trivial to implement in a separate
|
||||
coroutine. As a result the `enum event` and `async_receive_event`
|
||||
have been removed from the class too.
|
||||
|
||||
* Fixes a bug in `connection::async_receive_push` that prevented
|
||||
passing any response adapter other that `adapt(std::vector<node>)`.
|
||||
|
||||
* Changes the behaviour of `aedis::adapt()` that caused RESP3 errors
|
||||
to be ignored. One consequence of it is that `connection::async_run`
|
||||
would not exit with failure in servers that required authentication.
|
||||
|
||||
* Changes the behaviour of `connection::async_run` that would cause it
|
||||
to complete with success when an error in the
|
||||
`connection::async_exec` occurred.
|
||||
|
||||
* Ports the buildsystem from autotools to CMake.
|
||||
|
||||
== v1.0.0
|
||||
|
||||
* Adds experimental cmake support for windows users.
|
||||
|
||||
* Adds new class `aedis::sync` that wraps an `aedis::connection` in
|
||||
a thread-safe and synchronous API. All free functions from the
|
||||
`sync.hpp` are now member functions of `aedis::sync`.
|
||||
|
||||
* Split `aedis::connection::async_receive_event` in two functions, one
|
||||
to receive events and another for server side pushes, see
|
||||
`aedis::connection::async_receive_push`.
|
||||
|
||||
* Removes collision between `aedis::adapter::adapt` and
|
||||
`aedis::adapt`.
|
||||
|
||||
* Adds `connection::operation` enum to replace `cancel_*` member
|
||||
functions with a single cancel function that gets the operations
|
||||
that should be cancelled as argument.
|
||||
|
||||
* Bugfix: a bug on reconnect from a state where the `connection` object
|
||||
had unsent commands. It could cause `async_exec` to never
|
||||
complete under certain conditions.
|
||||
|
||||
* Bugfix: Documentation of `adapt()` functions were missing from
|
||||
Doxygen.
|
||||
|
||||
== v0.3.0
|
||||
|
||||
* Adds `experimental::exec` and `receive_event` functions to offer a
|
||||
thread safe and synchronous way of executing requests across
|
||||
threads. See `intro_sync.cpp` and `subscriber_sync.cpp` for
|
||||
examples.
|
||||
|
||||
* `connection::async_read_push` was renamed to `async_receive_event`.
|
||||
|
||||
* `connection::async_receive_event` is now being used to communicate
|
||||
internal events to the user, such as resolve, connect, push etc. For
|
||||
examples see cpp20_subscriber.cpp and `connection::event`.
|
||||
|
||||
* The `aedis` directory has been moved to `include` to look more
|
||||
similar to Boost libraries. Users should now replace `-I/aedis-path`
|
||||
with `-I/aedis-path/include` in the compiler flags.
|
||||
|
||||
* The `AUTH` and `HELLO` commands are now sent automatically. This change was
|
||||
necessary to implement reconnection. The username and password
|
||||
used in `AUTH` should be provided by the user on
|
||||
`connection::config`.
|
||||
|
||||
* Adds support for reconnection. See `connection::enable_reconnect`.
|
||||
|
||||
* Fixes a bug in the `connection::async_run(host, port)` overload
|
||||
that was causing crashes on reconnection.
|
||||
|
||||
* Fixes the executor usage in the connection class. Before these
|
||||
changes it was imposing `any_io_executor` on users.
|
||||
|
||||
* `connection::async_receiver_event` is not cancelled anymore when
|
||||
`connection::async_run` exits. This change makes user code simpler.
|
||||
|
||||
* `connection::async_exec` with host and port overload has been
|
||||
removed. Use the other `connection::async_run` overload.
|
||||
|
||||
* The host and port parameters from `connection::async_run` have been
|
||||
move to `connection::config` to better support authentication and
|
||||
failover.
|
||||
|
||||
* Many simplifications in the `chat_room` example.
|
||||
|
||||
* Fixes build in clang the compilers and makes some improvements in
|
||||
the documentation.
|
||||
|
||||
== v0.2.0-1
|
||||
|
||||
* Fixes a bug that happens on very high load. (v0.2.1)
|
||||
* Major rewrite of the high-level API. There is no more need to use the low-level API anymore.
|
||||
* No more callbacks: Sending requests follows the ASIO asynchronous model.
|
||||
* Support for reconnection: Pending requests are not canceled when a connection is lost and are re-sent when a new one is established.
|
||||
* The library is not sending HELLO-3 on user behalf anymore. This is important to support AUTH properly.
|
||||
|
||||
== v0.1.0-2
|
||||
|
||||
* Adds reconnect coroutine in the `echo_server` example. (v0.1.2)
|
||||
* Corrects `client::async_wait_for_data` with `make_parallel_group` to launch operation. (v0.1.2)
|
||||
* Improvements in the documentation. (v0.1.2)
|
||||
* Avoids dynamic memory allocation in the client class after reconnection. (v0.1.2)
|
||||
* Improves the documentation and adds some features to the high-level client. (v.0.1.1)
|
||||
* Improvements in the design and documentation.
|
||||
|
||||
== v0.0.1
|
||||
|
||||
* First release to collect design feedback.
|
||||
|
||||
121
doc/modules/ROOT/pages/comparison.adoc
Normal file
121
doc/modules/ROOT/pages/comparison.adoc
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Comparison with other Redis clients
|
||||
|
||||
## Comparison
|
||||
|
||||
The main reason for why I started writing Boost.Redis was to have a client
|
||||
compatible with the Asio asynchronous model. As I made progresses I could
|
||||
also address what I considered weaknesses in other libraries. Due to
|
||||
time constraints I won't be able to give a detailed comparison with
|
||||
each client listed in the
|
||||
https://redis.io/docs/clients/#cpp[official list].
|
||||
Instead, I will focus on the most popular pass:[C++] client on github in number of stars, namely:
|
||||
|
||||
https://github.com/sewenew/redis-plus-plus[]
|
||||
|
||||
### Boost.Redis vs Redis-plus-plus
|
||||
|
||||
Before we start it is important to mention some of the things
|
||||
redis-plus-plus does not support
|
||||
|
||||
* The latest version of the communication protocol RESP3. Without that it is impossible to support some important Redis features like client side caching, among other things.
|
||||
* Coroutines.
|
||||
* Reading responses directly in user data structures to avoid creating temporaries.
|
||||
* Error handling with support for error-code.
|
||||
* Cancellation.
|
||||
|
||||
The remaining points will be addressed individually. Let us first
|
||||
have a look at what sending a command a pipeline and a transaction
|
||||
look like
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
auto redis = Redis("tcp://127.0.0.1:6379");
|
||||
|
||||
// Send commands
|
||||
redis.set("key", "val");
|
||||
auto val = redis.get("key"); // val is of type OptionalString.
|
||||
if (val)
|
||||
std::cout << *val << std::endl;
|
||||
|
||||
// Sending pipelines
|
||||
auto pipe = redis.pipeline();
|
||||
auto pipe_replies = pipe.set("key", "value")
|
||||
.get("key")
|
||||
.rename("key", "new-key")
|
||||
.rpush("list", {"a", "b", "c"})
|
||||
.lrange("list", 0, -1)
|
||||
.exec();
|
||||
|
||||
// Parse reply with reply type and index.
|
||||
auto set_cmd_result = pipe_replies.get<bool>(0);
|
||||
// ...
|
||||
|
||||
// Sending a transaction
|
||||
auto tx = redis.transaction();
|
||||
auto tx_replies = tx.incr("num0")
|
||||
.incr("num1")
|
||||
.mget({"num0", "num1"})
|
||||
.exec();
|
||||
|
||||
auto incr_result0 = tx_replies.get<long long>(0);
|
||||
// ...
|
||||
----
|
||||
|
||||
Some of the problems with this API are
|
||||
|
||||
* Heterogeneous treatment of commands, pipelines and transaction. This makes auto-pipelining impossible.
|
||||
* Any Api that sends individual commands has a very restricted scope of usability and should be avoided for performance reasons.
|
||||
* The API imposes exceptions on users, no error-code overload is provided.
|
||||
* No way to reuse the buffer for new calls to e.g. redis.get in order to avoid further dynamic memory allocations.
|
||||
* Error handling of resolve and connection not clear.
|
||||
|
||||
According to the documentation, pipelines in redis-plus-plus have
|
||||
the following characteristics
|
||||
|
||||
> +NOTE+: By default, creating a Pipeline object is NOT cheap, since
|
||||
> it creates a new connection.
|
||||
|
||||
This is clearly a downside in the API as pipelines should be the
|
||||
default way of communicating and not an exception, paying such a
|
||||
high price for each pipeline imposes a severe cost in performance.
|
||||
Transactions also suffer from the very same problem:
|
||||
|
||||
> +NOTE+: Creating a Transaction object is NOT cheap, since it
|
||||
> creates a new connection.
|
||||
|
||||
In Boost.Redis there is no difference between sending one command, a
|
||||
pipeline or a transaction because requests are decoupled
|
||||
from the I/O objects.
|
||||
|
||||
> redis-plus-plus also supports async interface, however, async
|
||||
> support for Transaction and Subscriber is still on the way.
|
||||
>
|
||||
> The async interface depends on third-party event library, and so
|
||||
> far, only libuv is supported.
|
||||
|
||||
Async code in redis-plus-plus looks like the following
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
auto async_redis = AsyncRedis(opts, pool_opts);
|
||||
|
||||
Future<string> ping_res = async_redis.ping();
|
||||
|
||||
cout << ping_res.get() << endl;
|
||||
----
|
||||
|
||||
As the reader can see, the async interface is based on futures
|
||||
which is also known to have a bad performance. The biggest
|
||||
problem however with this async design is that it makes it
|
||||
impossible to write asynchronous programs correctly since it
|
||||
starts an async operation on every command sent instead of
|
||||
enqueueing a message and triggering a write when it can be sent.
|
||||
It is also not clear how are pipelines realised with this design
|
||||
(if at all).
|
||||
27
doc/modules/ROOT/pages/examples.adoc
Normal file
27
doc/modules/ROOT/pages/examples.adoc
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Examples
|
||||
|
||||
The examples below show how to use the features discussed throughout this documentation:
|
||||
|
||||
* {site-url}/example/cpp20_intro.cpp[cpp20_intro.cpp]: Does not use awaitable operators.
|
||||
* {site-url}/example/cpp20_intro_tls.cpp[cpp20_intro_tls.cpp]: Communicates over TLS.
|
||||
* {site-url}/example/cpp20_unix_sockets.cpp[cpp20_unix_sockets.cpp]: Communicates over UNIX domain sockets.
|
||||
* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions.
|
||||
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json.
|
||||
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf.
|
||||
* {site-url}/example/cpp20_resolve_with_sentinel.cpp[cpp20_resolve_with_sentinel.cpp]: Shows how to resolve a master address using sentinels.
|
||||
* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription.
|
||||
* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server.
|
||||
* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub.
|
||||
* {site-url}/example/cpp17_intro.cpp[cpp17_intro.cpp]: Uses callbacks and requires pass:[C++17].
|
||||
* {site-url}/example/cpp17_intro_sync.cpp[cpp17_intro_sync.cpp]: Runs `async_run` in a separate thread and performs synchronous calls to `async_exec`.
|
||||
* {site-url}/example/cpp17_spdlog.cpp[cpp17_spdlog.cpp]: Shows how to use third-party logging libraries like `spdlog` with Boost.Redis.
|
||||
|
||||
The main function used in some async examples has been factored out in
|
||||
the {site-url}/example/main.cpp[main.cpp] file.
|
||||
143
doc/modules/ROOT/pages/index.adoc
Normal file
143
doc/modules/ROOT/pages/index.adoc
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
[#intro]
|
||||
= Introduction
|
||||
|
||||
Boost.Redis is a high-level https://redis.io/[Redis] 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].
|
||||
|
||||
== Requirements
|
||||
|
||||
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).
|
||||
* 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].
|
||||
|
||||
== Building the library
|
||||
|
||||
To use the library it is necessary to include the following:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
#include <boost/redis/src.hpp>
|
||||
----
|
||||
|
||||
in exactly one source file in your applications. Otherwise, the library is header-only.
|
||||
|
||||
Boost.Redis unconditionally requires OpenSSL. Targets using Boost.Redis need to link
|
||||
to the OpenSSL libraries.
|
||||
|
||||
== Tutorial
|
||||
|
||||
The code below uses a short-lived connection to
|
||||
https://redis.io/commands/ping/[ping] a Redis server:
|
||||
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
|
||||
auto co_main(config const& cfg) -> net::awaitable<void>
|
||||
{
|
||||
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
|
||||
// A request containing only a ping command.
|
||||
request req;
|
||||
req.push("PING", "Hello world");
|
||||
|
||||
// Response object.
|
||||
response<std::string> resp;
|
||||
|
||||
// Executes the request.
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
|
||||
}
|
||||
----
|
||||
|
||||
The roles played by the `async_run` and `async_exec` functions are:
|
||||
|
||||
* xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`]: executes the commands contained in the
|
||||
request and stores the individual responses in the response object. Can
|
||||
be called from multiple places in your code concurrently.
|
||||
* xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`]: keeps the connection healthy. It takes care of hostname resolution, session establishment, health-checks, reconnection and coordination of low-level read and write operations. It should be called only once per connection, regardless of the number of requests to execute.
|
||||
|
||||
== Server pushes
|
||||
|
||||
Redis servers can also send a variety of pushes to the client. Some of
|
||||
them are:
|
||||
|
||||
* https://redis.io/docs/manual/pubsub/[Pubsub messages].
|
||||
* https://redis.io/docs/manual/keyspace-notifications/[Keyspace notifications].
|
||||
* 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
|
||||
called in the same connection that is being used to execute commands.
|
||||
The coroutine below shows how to use it:
|
||||
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response 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);
|
||||
|
||||
// Loop reading Redis pushes.
|
||||
for (;;) {
|
||||
error_code ec;
|
||||
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
// Use the response resp in some way and then clear it.
|
||||
...
|
||||
|
||||
consume_one(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
== Further reading
|
||||
|
||||
Here is a list of topics that you might be interested in:
|
||||
|
||||
* xref:requests_responses.adoc[More on requests and responses].
|
||||
* xref:serialization.adoc[Serializing and parsing into custom types].
|
||||
* xref:logging.adoc[Configuring logging].
|
||||
* xref:examples.adoc[Examples].
|
||||
45
doc/modules/ROOT/pages/logging.adoc
Normal file
45
doc/modules/ROOT/pages/logging.adoc
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
|
||||
= Logging
|
||||
|
||||
xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`]
|
||||
is a complex algorithm, with features like built-in reconnection.
|
||||
This can make configuration problems, like a misconfigured hostname, difficult to debug -
|
||||
Boost.Redis will keep retrying to connect to the same hostname over and over.
|
||||
For this reason, Boost.Redis incorporates a lightweight logging solution, and
|
||||
*will log some status messages to stderr by default*.
|
||||
|
||||
Logging can be customized by passing a
|
||||
xref:reference:boost/redis/logger.adoc[`logger`] object to the connection's constructor. For example, logging can be disabled by writing:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
asio::io_context ioc;
|
||||
connection conn {ioc, logger{logger::level::disabled}};
|
||||
----
|
||||
|
||||
Every message logged by the library is attached a
|
||||
https://en.wikipedia.org/wiki/Syslog#Severity_level[syslog-like severity]
|
||||
tag (a xref:reference:boost/redis/logger/level.adoc[`logger::level`]).
|
||||
You can filter messages by severity by creating a `logger` with a specific level:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
asio::io_context ioc;
|
||||
|
||||
// Logs to stderr messages with severity >= level::error.
|
||||
// This will hide all informational output.
|
||||
connection conn {ioc, logger{logger::level::error}};
|
||||
----
|
||||
|
||||
The `logger` constructor accepts a `std::function<void(logger::level, std::string_view)>`
|
||||
as second argument. If supplied, Boost.Redis will call this function when logging
|
||||
instead of printing to stderr. This can be used to integrate third-party logging
|
||||
libraries. See our {site-url}/example/cpp17_spdlog.cpp[spdlog integration example]
|
||||
for sample code.
|
||||
90
doc/modules/ROOT/pages/reference.adoc
Normal file
90
doc/modules/ROOT/pages/reference.adoc
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
|
||||
[#reference]
|
||||
= Reference
|
||||
|
||||
[width=100%,cols="5*"]
|
||||
|===
|
||||
|
||||
| *Connections*
|
||||
| *Requests and responses*
|
||||
| *Adapters*
|
||||
| *RESP3 protocol*
|
||||
| *Unstable low-level APIs*
|
||||
|
||||
|
|
||||
xref:reference:boost/redis/connection.adoc[`connection`]
|
||||
|
||||
xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
|
||||
|
||||
xref:reference:boost/redis/address.adoc[`address`]
|
||||
|
||||
xref:reference:boost/redis/config.adoc[`config`]
|
||||
|
||||
xref:reference:boost/redis/error.adoc[`error`]
|
||||
|
||||
xref:reference:boost/redis/logger.adoc[`logger`]
|
||||
|
||||
xref:reference:boost/redis/logger/level.adoc[`logger::level`]
|
||||
|
||||
xref:reference:boost/redis/operation.adoc[`operation`]
|
||||
|
||||
xref:reference:boost/redis/usage.adoc[`usage`]
|
||||
|
||||
|
||||
|
|
||||
xref:reference:boost/redis/ignore_t.adoc[`ignore_t`]
|
||||
|
||||
xref:reference:boost/redis/ignore.adoc[`ignore`]
|
||||
|
||||
xref:reference:boost/redis/request.adoc[`request`]
|
||||
|
||||
xref:reference:boost/redis/request/config.adoc[`request::config`]
|
||||
|
||||
xref:reference:boost/redis/response.adoc[`response`]
|
||||
|
||||
xref:reference:boost/redis/generic_response.adoc[`generic_response`]
|
||||
|
||||
xref:reference:boost/redis/consume_one-08.adoc[`consume_one`]
|
||||
|
||||
|
||||
|
|
||||
xref:reference:boost/redis/adapter/boost_redis_adapt.adoc[`boost_redis_adapt`]
|
||||
|
||||
xref:reference:boost/redis/adapter/ignore.adoc[`adapter::ignore`]
|
||||
|
||||
xref:reference:boost/redis/adapter/error.adoc[`adapter::error`]
|
||||
|
||||
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/node.adoc[`node`]
|
||||
|
||||
xref:reference:boost/redis/resp3/node_view.adoc[`node_view`]
|
||||
|
||||
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/is_aggregate.adoc[`is_aggregate`]
|
||||
|
||||
|
||||
|
|
||||
|
||||
xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parser.adoc[`parser`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parse.adoc[`parse`]
|
||||
|
||||
|===
|
||||
302
doc/modules/ROOT/pages/requests_responses.adoc
Normal file
302
doc/modules/ROOT/pages/requests_responses.adoc
Normal file
@@ -0,0 +1,302 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Requests and responses
|
||||
|
||||
== Requests
|
||||
|
||||
Redis requests are composed of one or more commands. In the
|
||||
Redis documentation, requests are called
|
||||
https://redis.io/topics/pipelining[pipelines]. For example:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Some example containers.
|
||||
std::list<std::string> list {...};
|
||||
std::map<std::string, mystruct> map { ...};
|
||||
|
||||
// The request can contain multiple commands.
|
||||
request req;
|
||||
|
||||
// Command with variable length of arguments.
|
||||
req.push("SET", "key", "some value", "EX", "2");
|
||||
|
||||
// Pushes a list.
|
||||
req.push_range("SUBSCRIBE", list);
|
||||
|
||||
// Same as above but as an iterator range.
|
||||
req.push_range("SUBSCRIBE", std::cbegin(list), std::cend(list));
|
||||
|
||||
// Pushes a map.
|
||||
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`]
|
||||
object when constructed that dictates how requests are handled in situations like
|
||||
reconnection. The reader is advised to read it carefully.
|
||||
|
||||
## Responses
|
||||
|
||||
Boost.Redis uses the following strategy to deal with Redis responses:
|
||||
|
||||
* xref:reference:boost/redis/response.adoc[`boost::redis::response`] should be used
|
||||
when the request's number of commands is known at compile-time.
|
||||
* xref:reference:boost/redis/generic_response.adoc[`boost::redis::generic_response`] should be
|
||||
used when the number of commands is dynamic.
|
||||
|
||||
For example, the request below has three commands:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("INCR", "key");
|
||||
req.push("QUIT");
|
||||
----
|
||||
|
||||
Therefore, its response will also contain three elements.
|
||||
The following response object can be used:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
response<std::string, int, std::string>
|
||||
----
|
||||
|
||||
The response behaves as a `std::tuple` and must
|
||||
have as many elements as the request has commands (exceptions below).
|
||||
It is also necessary that each tuple element is capable of storing the
|
||||
response to the command it refers to, otherwise an error will occur.
|
||||
|
||||
To ignore responses to individual commands in the request use the tag
|
||||
xref:reference:boost/redis/ignore_t.adoc[`boost::redis::ignore_t`]. For example:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Ignore the second and last responses.
|
||||
response<std::string, ignore_t, std::string, ignore_t>
|
||||
----
|
||||
|
||||
The following table provides the RESP3-types returned by some Redis
|
||||
commands:
|
||||
|
||||
[cols="3*"]
|
||||
|===
|
||||
|
||||
| *Command* | *RESP3 type* | *Documentation*
|
||||
|
||||
| `lpush` | Number | https://redis.io/commands/lpush[]
|
||||
| `lrange` | Array | https://redis.io/commands/lrange[]
|
||||
| `set` | Simple-string, null or blob-string | https://redis.io/commands/set[]
|
||||
| `get` | Blob-string | https://redis.io/commands/get[]
|
||||
| `smembers` | Set | https://redis.io/commands/smembers[]
|
||||
| `hgetall` | Map | https://redis.io/commands/hgetall[]
|
||||
|
||||
|===
|
||||
|
||||
To map these RESP3 types into a pass:[C++] data structure use the table below:
|
||||
|
||||
[cols="3*"]
|
||||
|===
|
||||
|
||||
| *RESP3 type* | *Possible pass:[C++] type* | *Type*
|
||||
|
||||
| Simple-string | `std::string` | Simple
|
||||
| Simple-error | `std::string` | Simple
|
||||
| Blob-string | `std::string`, `std::vector` | Simple
|
||||
| Blob-error | `std::string`, `std::vector` | Simple
|
||||
| Number | `long long`, `int`, `std::size_t`, `std::string` | Simple
|
||||
| Double | `double`, `std::string` | Simple
|
||||
| Null | `std::optional<T>` | Simple
|
||||
| Array | `std::vector`, `std::list`, `std::array`, `std::deque` | Aggregate
|
||||
| Map | `std::vector`, `std::map`, `std::unordered_map` | Aggregate
|
||||
| Set | `std::vector`, `std::set`, `std::unordered_set` | Aggregate
|
||||
| Push | `std::vector`, `std::map`, `std::unordered_map` | Aggregate
|
||||
|
||||
|===
|
||||
|
||||
For example, the response to the request
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
request req;
|
||||
req.push("HELLO", 3);
|
||||
req.push_range("RPUSH", "key1", vec);
|
||||
req.push_range("HSET", "key2", map);
|
||||
req.push("LRANGE", "key3", 0, -1);
|
||||
req.push("HGETALL", "key4");
|
||||
req.push("QUIT");
|
||||
----
|
||||
|
||||
Can be read in the following response object:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
response<
|
||||
redis::ignore_t, // hello
|
||||
int, // rpush
|
||||
int, // hset
|
||||
std::vector<T>, // lrange
|
||||
std::map<U, V>, // hgetall
|
||||
std::string // quit
|
||||
> resp;
|
||||
----
|
||||
|
||||
To execute the request and read the response use
|
||||
xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
co_await conn->async_exec(req, resp);
|
||||
----
|
||||
|
||||
If the intention is to ignore responses altogether, use
|
||||
xref:reference:boost/redis/ignore.adoc[`ignore`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Ignores the response
|
||||
co_await conn->async_exec(req, ignore);
|
||||
----
|
||||
|
||||
Responses that contain nested aggregates or heterogeneous data
|
||||
types will be given special treatment later in xref:#the-general-case[the general case]. As
|
||||
of this writing, not all RESP3 types are used by the Redis server,
|
||||
which means in practice users will be concerned with a reduced
|
||||
subset of the RESP3 specification.
|
||||
|
||||
### Pushes
|
||||
|
||||
Commands that have no response, like
|
||||
|
||||
* `"SUBSCRIBE"`
|
||||
* `"PSUBSCRIBE"`
|
||||
* `"UNSUBSCRIBE"`
|
||||
|
||||
must **NOT** be included in the response tuple. For example, the following request
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.push("QUIT");
|
||||
----
|
||||
|
||||
must be read in the response object `response<std::string, std::string>`.
|
||||
|
||||
### Null
|
||||
|
||||
It is not uncommon for apps to access keys that do not exist or that
|
||||
have already expired in the Redis server. To deal with these use cases,
|
||||
wrap the type within a `std::optional`:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
response<
|
||||
std::optional<A>,
|
||||
std::optional<B>,
|
||||
...
|
||||
> resp;
|
||||
----
|
||||
|
||||
Everything else stays the same.
|
||||
|
||||
### Transactions
|
||||
|
||||
To read responses to transactions we must first observe that Redis
|
||||
will queue the transaction commands and send their individual
|
||||
responses as elements of an array. The array itself is the response to
|
||||
the `EXEC` command. For example, to read the response to this request
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
req.push("MULTI");
|
||||
req.push("GET", "key1");
|
||||
req.push("LRANGE", "key2", 0, -1);
|
||||
req.push("HGETALL", "key3");
|
||||
req.push("EXEC");
|
||||
----
|
||||
|
||||
Use the following response type:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
response<
|
||||
ignore_t, // multi
|
||||
ignore_t, // QUEUED
|
||||
ignore_t, // QUEUED
|
||||
ignore_t, // QUEUED
|
||||
response<
|
||||
std::optional<std::string>, // get
|
||||
std::optional<std::vector<std::string>>, // lrange
|
||||
std::optional<std::map<std::string, std::string>> // hgetall
|
||||
> // exec
|
||||
> resp;
|
||||
----
|
||||
|
||||
For a complete example, see {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp].
|
||||
|
||||
[#the-general-case]
|
||||
### The general case
|
||||
|
||||
There are cases where responses to Redis
|
||||
commands won't fit in the model presented above. Some examples are:
|
||||
|
||||
* Commands (like `set`) whose responses don't have a fixed
|
||||
RESP3 type. Expecting an `int` and receiving a blob-string
|
||||
results in an error.
|
||||
* RESP3 aggregates that contain nested aggregates can't be read in STL containers.
|
||||
* Transactions with a dynamic number of commands can't be read in a `response`.
|
||||
|
||||
To deal with these cases Boost.Redis provides the xref:reference:boost/redis/resp3/node.adoc[`boost::redis::resp3::node`] type
|
||||
abstraction, that is the most general form of an element in a
|
||||
response, be it a simple RESP3 type or the element of an aggregate. It
|
||||
is defined like:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
template <class String>
|
||||
struct basic_node {
|
||||
// The RESP3 type of the data in this node.
|
||||
type data_type;
|
||||
|
||||
// The number of elements of an aggregate (or 1 for simple data).
|
||||
std::size_t aggregate_size;
|
||||
|
||||
// The depth of this node in the response tree.
|
||||
std::size_t depth;
|
||||
|
||||
// The actual data. For aggregate types this is always empty.
|
||||
String value;
|
||||
};
|
||||
----
|
||||
|
||||
Any response to a Redis command can be parsed into a
|
||||
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response].
|
||||
The vector can be seen as a pre-order view of the response tree.
|
||||
Using it is not different than using other types:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Receives any RESP3 simple or aggregate data type.
|
||||
boost::redis::generic_response resp;
|
||||
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.
|
||||
* `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`.
|
||||
|
||||
In addition to the above users can also use unordered versions of the
|
||||
containers. The same reasoning applies to sets e.g. `SMEMBERS`
|
||||
and other data structures in general.
|
||||
26
doc/modules/ROOT/pages/serialization.adoc
Normal file
26
doc/modules/ROOT/pages/serialization.adoc
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@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)
|
||||
//
|
||||
|
||||
= Serializing and parsing into custom types
|
||||
|
||||
Boost.Redis supports serialization of user defined types by means of
|
||||
the following customization points
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Serialize
|
||||
void boost_redis_to_bulk(std::string& to, mystruct const& obj);
|
||||
|
||||
// Deserialize
|
||||
void boost_redis_from_bulk(mystruct& u, node_view const& node, boost::system::error_code&);
|
||||
----
|
||||
|
||||
These functions are accessed over ADL and therefore they must be
|
||||
imported in the global namespace by the user. The following examples might be of interest:
|
||||
|
||||
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: serializes and parses JSON objects.
|
||||
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: serializes and parses https://protobuf.dev/[protobuf] objects.
|
||||
10
doc/mrdocs.cpp
Normal file
10
doc/mrdocs.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2025 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)
|
||||
//
|
||||
|
||||
#define BOOST_ALLOW_DEPRECATED // avoid mrdocs errors with the BOOST_DEPRECATED macro
|
||||
|
||||
#include <boost/redis.hpp>
|
||||
31
doc/mrdocs.yml
Normal file
31
doc/mrdocs.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
source-root: ../include
|
||||
compilation-database: ./CMakeLists.txt
|
||||
include-symbols:
|
||||
- "boost::redis::**"
|
||||
exclude-symbols:
|
||||
- "boost::redis::detail::**"
|
||||
- "boost::redis::adapter::detail::**"
|
||||
- "boost::redis::resp3::detail::**"
|
||||
- "boost::redis::basic_connection::run_is_canceled"
|
||||
- "boost::redis::basic_connection::this_type"
|
||||
- "boost::redis::any_adapter::impl_t"
|
||||
- "boost::redis::any_adapter::fn_type"
|
||||
- "boost::redis::any_adapter::create_impl"
|
||||
- "boost::redis::any_adapter::impl_"
|
||||
- "boost::redis::request::payload"
|
||||
- "boost::redis::request::has_hello_priority"
|
||||
see-below:
|
||||
- "boost::redis::adapter::ignore"
|
||||
sort-members: false
|
||||
base-url: https://github.com/boostorg/redis/blob/master/include/
|
||||
use-system-libc: true
|
||||
warn-as-error: true
|
||||
warn-if-undocumented: false
|
||||
warn-no-paramdoc: false
|
||||
671
doc/on-the-costs-of-async-abstractions.md
Normal file
671
doc/on-the-costs-of-async-abstractions.md
Normal file
@@ -0,0 +1,671 @@
|
||||
# On the costs of asynchronous abstractions
|
||||
|
||||
The biggest force behind the evolution of
|
||||
[Boost.Redis](https://github.com/boostorg/redis) was my struggling in
|
||||
coming up with a high-level connection abstraction that was capable of
|
||||
multiplexing Redis commands from independent sources while
|
||||
concurrently handling server pushes. This journey taught me many
|
||||
important lessons, many of which are related to the design and
|
||||
performance of asynchronous programs based on Boost.Asio.
|
||||
|
||||
In this article I will share some of the lessons learned, specially
|
||||
those related to the performance costs of _abstractions_ such as
|
||||
`async_read_until` that tend to overschedule into the event-loop. In
|
||||
this context I will also briefly comment on how the topics discussed
|
||||
here influenced my views on the proposed
|
||||
[P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
|
||||
(a.k.a. Senders and Receivers), which is likely to become the basis of
|
||||
networking in upcoming C++ standards.
|
||||
|
||||
Although the analysis presented in this article uses the Redis communication
|
||||
protocol for illustration I expect it to be useful in general since
|
||||
[RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) shares
|
||||
many similarities with other widely used protocols such as HTTP.
|
||||
|
||||
## Parsing `\r\n`-delimited messages
|
||||
|
||||
The Redis server communicates with its clients by exchanging data
|
||||
serialized in
|
||||
[RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) format.
|
||||
Among the data types supported by this specification, the
|
||||
`\r\n`-delimited messages are some of the most frequent in a typical
|
||||
session. The table below shows some examples
|
||||
|
||||
Command | Response | Wire format | RESP3 name
|
||||
---------|----------|---------------|---------------------
|
||||
PING | PONG | `+PONG\r\n` | simple-string
|
||||
INCR | 42 | `:42\r\n` | number
|
||||
GET | null | `_\r\n` | null
|
||||
|
||||
Redis also supports command pipelines, which provide a way of
|
||||
optimizing round-trip times by batching commands. A pipeline composed
|
||||
by the commands shown in the previous table look like this
|
||||
|
||||
```
|
||||
| Sent in a |
|
||||
| single write |
|
||||
+--------+ | | +-------+
|
||||
| | --------> PING + INCR + GET --------> | |
|
||||
| | | |
|
||||
| Client | | Redis |
|
||||
| | | |
|
||||
| | <-------- "+PONG\r\n:42\r\n_\r\n" <-------- | |
|
||||
+--------+ |<------>|<---->|<-->| +-------+
|
||||
| |
|
||||
| Responses |
|
||||
```
|
||||
|
||||
Messages that use delimiters are so common in networking that a
|
||||
facility called `async_read_until` for reading them incrementally from
|
||||
a socket is already part of Boost.Asio. The coroutine below uses it to
|
||||
print message contents to the screen
|
||||
|
||||
```cpp
|
||||
awaitable<void> parse_resp3_simple_msgs(tcp::socket socket)
|
||||
{
|
||||
for (std::string buffer;;) {
|
||||
auto n = co_await async_read_until(socket, dynamic_buffer(buffer), "\r\n");
|
||||
|
||||
std::cout << buffer.substr(1, n - 3) << std::endl;
|
||||
|
||||
// Consume the buffer.
|
||||
buffer.erase(0, n);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If we pay attention to the buffer content as it is parsed by the code
|
||||
above we can see it is rotated fairly often, for example
|
||||
|
||||
```
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n"
|
||||
":100\r\n+OK\r\n_\r\n"
|
||||
"+OK\r\n_\r\n"
|
||||
"_\r\n"
|
||||
""
|
||||
```
|
||||
|
||||
When I first realized these, apparently excessive, buffer rotations I
|
||||
was concerned they would impact the performance of Boost.Redis in a
|
||||
severe way. To measure the magnitude of this impact I came up with an
|
||||
experimental implementation of Asio's `dynamic_buffer` that consumed
|
||||
the buffer less eagerly than the `std::string::erase` function used
|
||||
above. For that, the implementation increased a buffer offset up
|
||||
to a certain threshold and only then triggered a (larger) rotation.
|
||||
This is illustrated in the diagram below
|
||||
|
||||
```
|
||||
|<---- offset threshold ---->|
|
||||
| |
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|
||||
| # Initial offset
|
||||
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|
||||
|<------>| # After 1st message
|
||||
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|
||||
|<-------------->| # After 2nd message
|
||||
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|
||||
|<--------------------->| # After 3rd message
|
||||
|
||||
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|
||||
|<-------------------------->| # Threshold crossed after the 4th message
|
||||
|
||||
"+PONG\r\n"
|
||||
| # After rotation
|
||||
```
|
||||
|
||||
After comparing the performance differences between the two versions I
|
||||
was surprised there wasn't any! But that was also very suspicious
|
||||
since some RESP3 aggregate types contain a considerable number of
|
||||
separators. For example, a map with two pairs `[(key1, value1),
|
||||
(key2, value2)]` encoded in RESP3 requires ten rotations in total
|
||||
|
||||
```
|
||||
"%2\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
|
||||
"$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
|
||||
"key1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
|
||||
"$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
|
||||
...
|
||||
```
|
||||
|
||||
It was evident something more costly was shadowing the buffer
|
||||
rotations. But it couldn't be the search for the separator since it
|
||||
performs equivalently to rotations. It is also easy to show that the
|
||||
overhead is not related to any IO operation since the problem persists
|
||||
if the buffer is never consumed (which causes the function to be
|
||||
called with the same string repeatedly). Once these two factors
|
||||
are removed from the table, we are driven into the conclusion that
|
||||
calling `async_read_until` has an intrinsic cost, let us see what
|
||||
that is.
|
||||
|
||||
### Async operations that complete synchronously considered harmful
|
||||
|
||||
Assume the scenario described earlier where `async_read_until` is used
|
||||
to parse multiple `\r\n`-delimited messages. The following is a
|
||||
detailed description of what happens behind the scenes
|
||||
|
||||
1. `async_read_until` calls `socket.async_read_some` repeatedly
|
||||
until the separator `\r\n` shows up in the buffer
|
||||
|
||||
```
|
||||
"<read1>" # Read 1: needs more data.
|
||||
"<read1><read2>" # Read 2: needs more data.
|
||||
"<read1><read2>" # Read 3: needs more data.
|
||||
"<read1><read2><read3>" # Read 4: needs more data.
|
||||
"<read1><read2><read3>\r\n<bonus bytes>" # separator found, done.
|
||||
```
|
||||
|
||||
2. The last call to `socket.async_read_some` happens to read past
|
||||
the separator `\r\n` (depicted as `<bonus bytes>` above),
|
||||
resulting in bonus (maybe incomplete) messages in the buffer
|
||||
|
||||
```
|
||||
| 1st async_read_some | 2nd async_read_some |
|
||||
| | |
|
||||
"+message content here \r\n:100\r\n+OK\r\n_\r\n+incomplete respo"
|
||||
| | | |
|
||||
| Message wanted |<-- bonus msgs --->|<--incomplete-->|
|
||||
| | msg |
|
||||
| | |
|
||||
| |<---------- bonus bytes ----------->|
|
||||
```
|
||||
|
||||
3. The buffer is consumed and `async_read_until` is called again.
|
||||
However, since the buffer already contains the next message this
|
||||
is an IO-less call
|
||||
|
||||
```
|
||||
":100\r\n+OK\r\n_\r\n+not enough byt"
|
||||
| | |
|
||||
| No IO required | Need more |
|
||||
| to parse these | data |
|
||||
| messages. | |
|
||||
```
|
||||
|
||||
The fact that step 3. doesn't perform any IO implies the operation can
|
||||
complete synchronously, but because this is an asynchronous function
|
||||
Boost.Asio by default won't call the continuation before the
|
||||
function returns. The implementation must therefore enqueue it for
|
||||
execution, as depicted below
|
||||
|
||||
```
|
||||
OP5 ---> OP4 ---> OP3 ---> OP2 ---> OP1 # Reschedules the continuation
|
||||
|
|
||||
OP1 schedules its continuation |
|
||||
+-----------------------------------+
|
||||
|
|
||||
|
|
||||
OP6 ---> OP5 ---> OP4 ---> OP3 ---> OP2 # Reschedules the continuation
|
||||
|
|
||||
OP2 schedules its continuation |
|
||||
+-----------------------------------+
|
||||
|
|
||||
|
|
||||
OP7 ---> OP6 ---> OP5 ---> OP4 ---> OP3
|
||||
```
|
||||
|
||||
When summed up, the excessive rescheduling of continuations lead to
|
||||
performance degradation at scale. But since this is an event-loop
|
||||
there is no way around rescheduling as doing otherwise would mean
|
||||
allowing a task to monopolize the event-loop, preventing other tasks
|
||||
from making progress. The best that can be done is to avoid
|
||||
_overscheduling_, so let us determine how much rescheduling is too
|
||||
much.
|
||||
|
||||
## The intrinsic latency of an event-loop
|
||||
|
||||
An event-loop is a design pattern originally used to handle events
|
||||
external to the application, such as GUIs, networking and other forms
|
||||
of IO. If we take this literally, it becomes evident that the way
|
||||
`async_read_until` works is incompatible with an event-loop since
|
||||
_searching for the separator_ is not an external event and as such
|
||||
should not have to be enqueued for execution.
|
||||
|
||||
Once we constrain ourselves to events that have an external origin,
|
||||
such as anything related to IO and including any form of IPC, the
|
||||
scheduling overhead is reduced considerably since the latency
|
||||
of the transport layer eclipses whatever time it takes to schedule the
|
||||
continuation, for example, according to
|
||||
[these](https://www.boost.org/doc/libs/develop/libs/cobalt/doc/html/index.html#posting_to_an_executor)
|
||||
benchmarks, the time it takes to schedule a task in the
|
||||
`asio::io_context ` is approximately `50ns`.
|
||||
|
||||
To give the reader an idea about the magnitude of this number, if
|
||||
rescheduling alone were to account for 1% of the runtime of an app
|
||||
that uses asynchronous IO to move around data in chunks of size 128kb,
|
||||
then this app would have a throughput of approximately 24Gbs. At such
|
||||
high throughput multiple other factors kick in before any scheduling
|
||||
overhead even starts to manifest.
|
||||
|
||||
It is therefore safe to say that only asynchronous operations that
|
||||
don't perform or are not bound to any IO are ever likely to
|
||||
overschedule in the sense described above. Those cases can be usually
|
||||
avoided, this is what worked for Boost.Redis
|
||||
|
||||
1. `async_read_until` was replaced with calls to
|
||||
`socket.async_read_some` and an incremental parser that does not
|
||||
do any IO.
|
||||
|
||||
2. Channel `try_` functions are used to check if send and receive
|
||||
operations can be called without suspension. For example,
|
||||
`try_send` before `async_send` and `try_receive` before
|
||||
`async_receive` ([see also](https://github.com/chriskohlhoff/asio/commit/fe4fd7acf145335eeefdd19708483c46caeb45e5)
|
||||
`try_send_via_dispatch` for a more aggressive optimization).
|
||||
|
||||
3. Coalescing of individual requests into a single payload to reduce
|
||||
the number of necessary writes on the socket, this is only
|
||||
possible because Redis supports pipelining (good protocols
|
||||
help!).
|
||||
|
||||
4. Increased the socket read sizes to 4kb to reduce the number of
|
||||
reads (which is outweighed by the costs of rotating data in the
|
||||
buffer).
|
||||
|
||||
5. Dropped the `resp3::async_read` abstraction. When I started
|
||||
developing Boost.Redis there was convincing precedent for having
|
||||
a `resp3::async_read` function to read complete RESP3 messages
|
||||
from a socket
|
||||
|
||||
Name | Description
|
||||
---------------------------------------|-------------------
|
||||
`asio::ip::tcp::async_read` | Reads `n` bytes from a stream.
|
||||
`beast::http::async_read` | Reads a complete HTTP message.
|
||||
`beast::websocket::stream::async_read` | Reads a complete Websocket message.
|
||||
`redis::async_read` | Reads a complete RESP3 message.
|
||||
|
||||
It turns out however that this function is also vulnerable to
|
||||
immediate completions since in command pipelines multiple
|
||||
responses show up in the buffer after a call to
|
||||
`socket.async_read_some`. When that happens each call to
|
||||
`resp3::async_read` is IO-less.
|
||||
|
||||
Sometimes it is not possible to avoid asynchronous operations that
|
||||
complete synchronously, in the following sections we will see how to
|
||||
favor throughput over fairness in Boost.Asio.
|
||||
|
||||
### Calling the continuation inline
|
||||
|
||||
In Boost.Asio it is possible to customize how an algorithm executes
|
||||
the continuation when an immediate completion occurs, this includes
|
||||
the ability of calling it inline, thereby avoiding the costs of
|
||||
excessive rescheduling. Here is how it works
|
||||
|
||||
```cpp
|
||||
// (default) The continuation is enqueued for execution, regardless of
|
||||
// whether it is immediate or not.
|
||||
async_read_until(socket, buffer, "\r\n", continuation);
|
||||
|
||||
// Immediate completions are executed in exec2 (otherwise equal to the
|
||||
// version above). The completion is called inline if exec2 is the
|
||||
// same executor that is running the operation.
|
||||
async_read_until(socket, buffer, "\r\n", bind_immediate_executor(exec2, completion));
|
||||
```
|
||||
|
||||
To compare the performance of both cases I have written a small
|
||||
function that calls `async_read_until` in a loop with a buffer that is
|
||||
never consumed so that all completions are immediate. The version
|
||||
below uses the default behaviour
|
||||
|
||||
```cpp
|
||||
void read_safe(tcp::socket& s, std::string& buffer)
|
||||
{
|
||||
auto continuation = [&s, &buffer](auto ec, auto n)
|
||||
{
|
||||
read_safe(s, buffer); // Recursive call
|
||||
};
|
||||
|
||||
// This won't cause stack exhaustion because the continuation is
|
||||
// not called inline but posted in the event loop.
|
||||
async_read_until(s, dynamic_buffer(buffer), "\r\n", continuation);
|
||||
}
|
||||
```
|
||||
|
||||
To optimize away some of the rescheduling the version below uses the
|
||||
`bind_immediate_executor` customization to call the continuation
|
||||
reentrantly and then breaks the stack from time to time to avoid
|
||||
exhausting it
|
||||
|
||||
```cpp
|
||||
void read_reentrant(tcp::socket& s, std::string& buffer)
|
||||
{
|
||||
auto cont = [&](auto, auto)
|
||||
{
|
||||
read_reentrant(s, buffer); // Recursive call
|
||||
};
|
||||
|
||||
// Breaks the callstack after 16 inline calls.
|
||||
if (counter % 16 == 0) {
|
||||
post(s.get_executor(), [cont](){cont({}, 0);});
|
||||
return;
|
||||
}
|
||||
|
||||
// Continuation called reentrantly.
|
||||
async_read_until(s, dynamic_buffer(buffer), "\r\n",
|
||||
bind_immediate_executor(s.get_executor(), cont));
|
||||
}
|
||||
```
|
||||
|
||||
The diagram below shows what the reentrant chain of calls in the code
|
||||
above look like from the event-loop point of view
|
||||
|
||||
```
|
||||
OP5 ---> OP4 ---> OP3 ---> OP2 ---> OP1a # Completes immediately
|
||||
|
|
||||
|
|
||||
... |
|
||||
OP1b # Completes immediately
|
||||
|
|
||||
Waiting for OP5 to |
|
||||
reschedule its |
|
||||
continuation OP1c # Completes immediately
|
||||
|
|
||||
|
|
||||
... |
|
||||
OP1d # Break the call-stack
|
||||
|
|
||||
+-----------------------------------+
|
||||
|
|
||||
OP6 ---> OP5 ---> OP4 ---> OP3 ---> OP2
|
||||
```
|
||||
|
||||
Unsurprisingly, the reentrant code is 3x faster than the one that
|
||||
relies on the default behaviour (don't forget that this is a best case
|
||||
scenario, in the general case not all completions are immediate).
|
||||
Although faster, this strategy has some downsides
|
||||
|
||||
- The overall operation is not as fast as possible since it still
|
||||
has to reschedule from time to time to break the call stack. The
|
||||
less it reschedules the higher the risk of exhausting it.
|
||||
|
||||
- It is too easy to forget to break the stack. For example, the
|
||||
programmer might decide to branch somewhere into another chain of
|
||||
asynchronous calls that also use this strategy. To avoid
|
||||
exhaustion all such branches would have to be safeguarded with a
|
||||
manual rescheduling i.e. `post`.
|
||||
|
||||
- Requires additional layers of complexity such as
|
||||
`bind_immediate_executor` in addition to `bind_executor`.
|
||||
|
||||
- Non-compliat with more strict
|
||||
[guidelines](https://en.wikipedia.org/wiki/The_Power_of_10:_Rules_for_Developing_Safety-Critical_Code)
|
||||
that prohibits reentrat code.
|
||||
|
||||
- There is no simple way of choosing the maximum allowed number of
|
||||
reentrant calls for each function in a way that covers different
|
||||
use cases and users. Library writers and users would be tempted
|
||||
into using a small value reducing the performance advantage.
|
||||
|
||||
- If the socket is always ready for reading the task will
|
||||
monopolize IO for up to `16` interactions which might cause
|
||||
stutter in unrelated tasks as depicted below
|
||||
|
||||
```
|
||||
Unfairness
|
||||
|
||||
+----+----+----+ +----+----+----+ +----+----+----+
|
||||
Socket-1 | | | | | | | | | | | |
|
||||
+----+----+----+----+----+----+----+----+----+----+----+----+
|
||||
Socket-2 | | | | | |
|
||||
+----+ +----+ +----+
|
||||
```
|
||||
|
||||
From the aesthetic point of view the code above is also unpleasant as
|
||||
it breaks the function asynchronous contract by injecting a reentrant
|
||||
behaviour. It gives me the same kind of feeling I have about
|
||||
[recursive
|
||||
mutexes](http://www.zaval.org/resources/library/butenhof1.html).
|
||||
|
||||
Note: It is worth mentioning here that a similar
|
||||
[strategy](https://github.com/NVIDIA/stdexec/blob/6f23dd5b1d523541ce28af32fc2603403ebd36ed/include/exec/trampoline_scheduler.hpp#L52)
|
||||
is used to break the call stack of repeating algorithms in
|
||||
[stdexec](https://github.com/NVIDIA/stdexec), but in this time
|
||||
based on
|
||||
[P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
|
||||
and not on Boost.Asio.
|
||||
|
||||
### Coroutine tail-calls
|
||||
|
||||
In the previous section we have seen how to avoid overscheduling by
|
||||
instructing the asynchronous operation to call the completion inline
|
||||
on immediate completion. It turns out however that coroutine support
|
||||
for _tail-calls_ provide a way to completely sidestep this problem.
|
||||
This feature is described by
|
||||
[Lewis Baker](https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer)
|
||||
as follows
|
||||
|
||||
> A tail-call is one where the current stack-frame is popped before
|
||||
> the call and the current function’s return address becomes the
|
||||
> return-address for the callee. ie. the callee will return directly
|
||||
> the the [sic] caller of this function.
|
||||
|
||||
This means (at least in principle) that a library capable of using
|
||||
tail-calls when an immediate completion occurs neither has to
|
||||
reschedule the continuation nor call it inline. To test how this
|
||||
feature compares to the other styles I have used Boost.Cobalt. The
|
||||
code looks as follows
|
||||
|
||||
```cpp
|
||||
// Warning: risks unfairness and starvation of other tasks.
|
||||
task<void> read_until_unfair()
|
||||
{
|
||||
for (int i = 0; i != repeat; ++i) {
|
||||
co_await async_read_until(s, dynamic_buffer(buffer), "\r\n", cobalt::use_op);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The result of this comparison as listed in the table below
|
||||
|
||||
Time/s | Style | Configuration | Library
|
||||
-------|-----------|-----------------------------|-------------
|
||||
1,0 | Coroutine | `await_ready` optimization | Boost.Cobalt
|
||||
4.8 | Callback | Reentant | Boost.Asio
|
||||
10.3 | Coroutine | `use_op` | Boost.Cobalt
|
||||
14.9 | Callback | Regular | Boost.Asio
|
||||
15.6 | Coroutine | `asio::deferred` | Boost.Asio
|
||||
|
||||
As the reader can see, `cobalt::use_op` ranks 3rd and is considerably
|
||||
faster (10.3 vs 15.6) than the Asio equivalent that uses
|
||||
default-rescheduling. However, by trading rescheduling with tail-calls
|
||||
the code above can now monopolize the event-loop, resulting in
|
||||
unfairness if the socket happens to receive data at a higher rate
|
||||
than other tasks. If by chance data is received continuously
|
||||
on a socket that is always ready for reading, other tasks will starve
|
||||
|
||||
```
|
||||
Starvation
|
||||
|
||||
+----+----+----+----+----+----+----+----+----+----+----+----+
|
||||
Socket-1 | | | | | | | | | | | | |
|
||||
+----+----+----+----+----+----+----+----+----+----+----+----+
|
||||
|
||||
Socket-2 Starving ...
|
||||
|
||||
```
|
||||
|
||||
To avoid this problem the programmer is forced to reschedule from time
|
||||
to time, in the same way we did for the reentrant calls
|
||||
|
||||
```cpp
|
||||
task<void> read_until_fair()
|
||||
{
|
||||
for (int i = 0; i != repeat; ++i) {
|
||||
if (repeat % 16 == 0) {
|
||||
// Reschedules to address unfairness and starvation of
|
||||
// other tasks.
|
||||
co_await post(cobalt::use_op);
|
||||
continue;
|
||||
}
|
||||
|
||||
co_await async_read_until(s, dynamic_buffer(buffer), "\r\n", cobalt::use_op);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Delegating fairness-safety to applications is a dangerous game.
|
||||
This is a
|
||||
[problem](https://tokio.rs/blog/2020-04-preemption) the Tokio
|
||||
community had to deal with before Tokio runtime started enforcing
|
||||
rescheduling (after 256 successful operations)
|
||||
|
||||
> If data is received faster than it can be processed, it is possible
|
||||
> that more data will have already been received by the time the
|
||||
> processing of a data chunk completes. In this case, .await will
|
||||
> never yield control back to the scheduler, other tasks will not be
|
||||
> scheduled, resulting in starvation and large latency variance.
|
||||
|
||||
> Currently, the answer to this problem is that the user of Tokio is
|
||||
> responsible for adding yield points in both the application and
|
||||
> libraries. In practice, very few actually do this and end up being
|
||||
> vulnerable to this sort of problem.
|
||||
|
||||
### Safety in P2300 (Senders and Receivers)
|
||||
|
||||
As of this writing, the C++ standards committee (WG21) has been
|
||||
pursuing the standardization of a networking library for almost 20
|
||||
years. One of the biggest obstacles that prevented it from happening
|
||||
was a disagreement on what the _asynchronous model_ that underlies
|
||||
networking should look like. Until 2021 that model was basically
|
||||
Boost.Asio _executors_, but in this
|
||||
[poll](https://www.reddit.com/r/cpp/comments/q6tgod/c_committee_polling_results_for_asynchronous/)
|
||||
the committee decided to abandon that front and concentrate efforts on
|
||||
the new [P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
|
||||
proposal, also known as _senders and receivers_. The decision was
|
||||
quite [abrupt](https://isocpp.org/files/papers/P2464R0.html)
|
||||
|
||||
> The original plan about a week earlier than the actual writing of
|
||||
> this paper was to write a paper that makes a case for standardizing
|
||||
> the Networking TS.
|
||||
|
||||
and opinions turned out to be very strong against Boost.Asio (see
|
||||
[this](https://api.csswg.org/bikeshed/?force=1&url=https://raw.githubusercontent.com/brycelelbach/wg21_p2459_2022_january_library_evolution_poll_outcomes/main/2022_january_library_evolution_poll_outcomes.bs)
|
||||
for how each voter backed their vote)
|
||||
|
||||
> The whole concept is completely useless, there's no composed code
|
||||
> you can write with it.
|
||||
|
||||
The part of that debate that interests us most here is stated in
|
||||
[P2471](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2471r1.pdf),
|
||||
that compares Boost.Asio with P2300
|
||||
|
||||
> Yes, default rescheduling each operation and default not
|
||||
> rescheduling each operation, is a poor trade off. IMO both options
|
||||
> are poor. The one good option that I know of that can prevent stack
|
||||
> exhaustion is first-class tail-recursion in library or language
|
||||
|
||||
> ASIO has chosen to require that every async operation must schedule
|
||||
> the completion on a scheduler (every read, every write, etc..).
|
||||
|
||||
> sender/receiver has not decided to
|
||||
> require that the completion be scheduled.
|
||||
|
||||
> This is why I consider tail-call the only good solution. Scheduling
|
||||
> solutions are all inferior (give thanks to Lewis for this shift in
|
||||
> my understanding :) ).
|
||||
|
||||
Although tail-calls solve the problem of stack-exhaustion as we have
|
||||
seen above, it makes the code vulnerable to unfairness and starvation
|
||||
and therefore it is not an alternative to default-rescheduling as the
|
||||
quotation above is implying. To deal with the lack of
|
||||
default-rescheduling, libraries and applications built on top of P2300
|
||||
have to address the aforementioned problems, layer after layer. For
|
||||
example,
|
||||
[stdexec](https://github.com/NVIDIA/stdexec) has invented something
|
||||
called
|
||||
_[trampoline-scheduler](https://github.com/NVIDIA/stdexec/blob/e7cd275273525dbc693f4bf5f6dc4d4181b639e4/include/exec/trampoline_scheduler.hpp)_
|
||||
to protect repeating algorithms such as `repeat_effect_until` from
|
||||
exhausting the stack. This construct however is built around
|
||||
reentracy, allowing
|
||||
[sixteen](https://github.com/NVIDIA/stdexec/blob/83cdb92d316e8b3bca1357e2cf49fc39e9bed403/include/exec/trampoline_scheduler.hpp#L52)
|
||||
levels of inline calls by default. While in Boost.Asio it is possible to use
|
||||
reentracy as an optimization for a corner cases, here it is made its
|
||||
_modus operandi_, the downsides of this approach have already been stated in a
|
||||
previous section so I won't repeat it here.
|
||||
|
||||
Also the fact that a special scheduler is needed by specific
|
||||
algorithms is a problem on its own since it contradicts one of the
|
||||
main selling points of P2300 which is that of being _generic_. For
|
||||
example, [P2464R0](https://isocpp.org/files/papers/P2464R0.html) uses
|
||||
the code below as an example
|
||||
|
||||
```cpp
|
||||
void
|
||||
run_that_io_operation(
|
||||
scheduler auto sched,
|
||||
sender_of<network_buffer> auto wrapping_continuation)
|
||||
{
|
||||
// snip
|
||||
}
|
||||
```
|
||||
|
||||
and states
|
||||
|
||||
> I have no idea what the sched's concrete type is. I have no idea
|
||||
> what the wrapping_continuation's concrete type is. They're none of
|
||||
> my business, ...
|
||||
|
||||
Hence, by being generic, the algorithms built on top of P2300 are also
|
||||
unsafe (against stack-exhaustion, unfairness and starvation). Otherwise,
|
||||
if library writers require a specific scheduler to ensure safety, then
|
||||
the algorithms become automatically non-generic, pick your poison!
|
||||
|
||||
The proposers of P2300 claim that it doesn't address safety because it
|
||||
should be seen as the low-level building blocks of asynchronous
|
||||
programming and that its the role of higher-level libraries, to deal
|
||||
with that. This claim however does not hold since, as we have just
|
||||
seen, Boost.Asio also provides those building blocks but does so in a
|
||||
safe way. In fact during the whole development of Boost.Redis I never
|
||||
had to think about these kinds of problems because safety is built
|
||||
from the ground up.
|
||||
|
||||
### Avoiding coroutine suspension with `await_ready`
|
||||
|
||||
Now let us get back to the first place in the table above, which uses
|
||||
the `await_ready` optimization from Boost.Cobalt. This API provides
|
||||
users with the ability to avoid coroutine suspension altogether in
|
||||
case the separator is already present in the buffer. It works by
|
||||
defining a `struct` with the following interface
|
||||
|
||||
```cpp
|
||||
struct read_until : cobalt::op<error_code, std::size_t> {
|
||||
...
|
||||
|
||||
void ready(cobalt::handler<error_code, std::size_t> handler) override
|
||||
{
|
||||
// Search for the separator in buffer and call the handler if found
|
||||
}
|
||||
|
||||
void initiate(cobalt::completion_handler<error_code, std::size_t> complete) override
|
||||
{
|
||||
// Regular call to async_read_until.
|
||||
async_read_until(socket, buffer, delim, std::move(complete));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
and the code that uses it
|
||||
|
||||
```cpp
|
||||
for (int i = 0; i != repeat; ++i) {
|
||||
co_await read_until(socket, dynamic_buffer(buffer));
|
||||
}
|
||||
```
|
||||
|
||||
In essence, what the code above does is to skip a call to
|
||||
`async_read_unil` by first checking with the ready function whether
|
||||
the forthcoming operation is going to complete immediately. The
|
||||
nice thing about it is that the programmer can use this optimization
|
||||
only when a performance bottleneck is detected, without planing for it
|
||||
in advance. The drawback however is that it requires reimplementing
|
||||
the search for the separator in the body of the `ready` function,
|
||||
defeating the purpose of using `async_read_until` in first place as
|
||||
(again) it would have been simpler to reformulate the operation in
|
||||
terms of `socket.async_read_some` directly.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thanks to Klemens Morgenstern for answering questions about
|
||||
Boost.Cobalt.
|
||||
|
||||
1955
doc/package-lock.json
generated
Normal file
1955
doc/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
doc/package.json
Normal file
6
doc/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@cppalliance/antora-cpp-reference-extension": "^0.0.6",
|
||||
"antora": "^3.1.10"
|
||||
}
|
||||
}
|
||||
45
doc/redis-playbook.yml
Normal file
45
doc/redis-playbook.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
#
|
||||
# Copyright (c) 2025 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)
|
||||
#
|
||||
|
||||
site:
|
||||
url: https://github.com/boostorg/redis/blob/master
|
||||
title: Boost.Redis
|
||||
robots: allow
|
||||
start_page: redis::index.adoc
|
||||
|
||||
antora:
|
||||
extensions:
|
||||
- require: '@cppalliance/antora-cpp-reference-extension'
|
||||
dependencies:
|
||||
- name: 'boost'
|
||||
repo: 'https://github.com/boostorg/boost.git'
|
||||
tag: 'develop'
|
||||
variable: 'BOOST_SRC_DIR'
|
||||
system-env: 'BOOST_SRC_DIR'
|
||||
|
||||
asciidoc:
|
||||
attributes:
|
||||
# Scrolling problems appear without this
|
||||
page-pagination: ''
|
||||
|
||||
content:
|
||||
sources:
|
||||
- url: ..
|
||||
start_path: doc
|
||||
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/boostorg/website-v2-docs/releases/download/ui-master/ui-bundle.zip
|
||||
snapshot: true
|
||||
output_dir: _
|
||||
|
||||
output:
|
||||
dir: html
|
||||
|
||||
runtime:
|
||||
log:
|
||||
failure_level: error
|
||||
62
example/CMakeLists.txt
Normal file
62
example/CMakeLists.txt
Normal file
@@ -0,0 +1,62 @@
|
||||
add_library(examples_main STATIC main.cpp)
|
||||
target_compile_features(examples_main PRIVATE cxx_std_20)
|
||||
target_link_libraries(examples_main PRIVATE boost_redis_project_options)
|
||||
|
||||
macro(make_example EXAMPLE_NAME STANDARD)
|
||||
add_executable(${EXAMPLE_NAME} ${EXAMPLE_NAME}.cpp)
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_src)
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_project_options)
|
||||
target_compile_features(${EXAMPLE_NAME} PRIVATE cxx_std_${STANDARD})
|
||||
if (${STANDARD} STREQUAL "20")
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE examples_main)
|
||||
endif()
|
||||
if (${EXAMPLE_NAME} STREQUAL "cpp20_json")
|
||||
target_link_libraries(${EXAMPLE_NAME} PRIVATE Boost::json Boost::container_hash)
|
||||
endif()
|
||||
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()
|
||||
endmacro()
|
||||
|
||||
make_testable_example(cpp17_intro 17)
|
||||
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_example(cpp20_subscriber 20)
|
||||
make_example(cpp20_streams 20)
|
||||
make_example(cpp20_echo_server 20)
|
||||
make_example(cpp20_resolve_with_sentinel 20)
|
||||
|
||||
# We test the protobuf example only on gcc.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
find_package(Protobuf)
|
||||
if (Protobuf_FOUND)
|
||||
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS person.proto)
|
||||
make_testable_example(cpp20_protobuf 20)
|
||||
target_sources(cpp20_protobuf PUBLIC ${PROTO_SRCS} ${PROTO_HDRS})
|
||||
target_link_libraries(cpp20_protobuf PRIVATE ${Protobuf_LIBRARIES})
|
||||
target_include_directories(cpp20_protobuf PUBLIC ${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT MSVC)
|
||||
make_example(cpp20_chat_room 20)
|
||||
endif()
|
||||
|
||||
# We build and test the spdlog integration example only if the library is found
|
||||
find_package(spdlog)
|
||||
if (spdlog_FOUND)
|
||||
make_testable_example(cpp17_spdlog 17)
|
||||
target_link_libraries(cpp17_spdlog PRIVATE spdlog::spdlog)
|
||||
else()
|
||||
message(STATUS "Skipping the spdlog example because the spdlog package couldn't be found")
|
||||
endif()
|
||||
@@ -5,16 +5,18 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace net = boost::asio;
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
|
||||
auto main(int argc, char * argv[]) -> int
|
||||
auto main(int argc, char* argv[]) -> int
|
||||
{
|
||||
try {
|
||||
config cfg;
|
||||
@@ -29,10 +31,10 @@ auto main(int argc, char * argv[]) -> int
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
net::io_context ioc;
|
||||
asio::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
conn.async_run(cfg, {}, net::detached);
|
||||
conn.async_run(cfg, asio::detached);
|
||||
|
||||
conn.async_exec(req, resp, [&](auto ec, auto) {
|
||||
if (!ec)
|
||||
@@ -47,4 +49,3 @@ auto main(int argc, char * argv[]) -> int
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@
|
||||
|
||||
#include "sync_connection.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using boost::redis::sync_connection;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
|
||||
auto main(int argc, char * argv[]) -> int
|
||||
auto main(int argc, char* argv[]) -> int
|
||||
{
|
||||
try {
|
||||
config cfg;
|
||||
102
example/cpp17_spdlog.cpp
Normal file
102
example/cpp17_spdlog.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string_view>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace redis = boost::redis;
|
||||
|
||||
// Maps a Boost.Redis log level to a spdlog log level
|
||||
static spdlog::level::level_enum to_spdlog_level(redis::logger::level lvl)
|
||||
{
|
||||
switch (lvl) {
|
||||
// spdlog doesn't include the emerg and alert syslog levels,
|
||||
// so we convert them to the highest supported level.
|
||||
// Similarly, notice is similar to info
|
||||
case redis::logger::level::emerg:
|
||||
case redis::logger::level::alert:
|
||||
case redis::logger::level::crit: return spdlog::level::critical;
|
||||
case redis::logger::level::err: return spdlog::level::err;
|
||||
case redis::logger::level::warning: return spdlog::level::warn;
|
||||
case redis::logger::level::notice:
|
||||
case redis::logger::level::info: return spdlog::level::info;
|
||||
case redis::logger::level::debug:
|
||||
default: return spdlog::level::debug;
|
||||
}
|
||||
}
|
||||
|
||||
// This function glues Boost.Redis logging and spdlog.
|
||||
// It should have the signature shown here. It will be invoked
|
||||
// by Boost.Redis whenever a message is to be logged.
|
||||
static void do_log(redis::logger::level level, std::string_view msg)
|
||||
{
|
||||
spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Create a connection to connect to Redis, and pass it a custom logger.
|
||||
// Boost.Redis will call do_log whenever it needs to log a message.
|
||||
// Note that the function will only be called for messages with level >= info
|
||||
// (i.e. filtering is done by Boost.Redis).
|
||||
redis::connection conn{
|
||||
ioc,
|
||||
redis::logger{redis::logger::level::info, do_log}
|
||||
};
|
||||
|
||||
// Configuration to connect to the server
|
||||
redis::config cfg;
|
||||
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
|
||||
conn.async_run(cfg, asio::detached);
|
||||
|
||||
// Execute a request
|
||||
redis::request req;
|
||||
req.push("PING", "Hello world");
|
||||
|
||||
redis::response<std::string> resp;
|
||||
|
||||
conn.async_exec(req, resp, [&](boost::system::error_code ec, std::size_t /* bytes_read*/) {
|
||||
if (ec) {
|
||||
spdlog::error("Request failed: {}", ec.what());
|
||||
exit(1);
|
||||
} else {
|
||||
spdlog::info("PING: {}", std::get<0>(resp).value());
|
||||
}
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
// Actually run our example. Nothing will happen until we call run()
|
||||
ioc.run();
|
||||
|
||||
} catch (std::exception const& e) {
|
||||
spdlog::error("Error: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -5,83 +5,88 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||
#include <unistd.h>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
|
||||
|
||||
namespace net = boost::asio;
|
||||
using stream_descriptor = net::deferred_t::as_default_on_t<net::posix::stream_descriptor>;
|
||||
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
|
||||
using boost::redis::request;
|
||||
using boost::redis::generic_response;
|
||||
namespace asio = boost::asio;
|
||||
using asio::posix::stream_descriptor;
|
||||
using asio::signal_set;
|
||||
using boost::asio::async_read_until;
|
||||
using boost::asio::awaitable;
|
||||
using boost::asio::co_spawn;
|
||||
using boost::asio::consign;
|
||||
using boost::asio::detached;
|
||||
using boost::asio::dynamic_buffer;
|
||||
using boost::asio::redirect_error;
|
||||
using boost::asio::use_awaitable;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::ignore;
|
||||
using net::redirect_error;
|
||||
using net::use_awaitable;
|
||||
using boost::redis::request;
|
||||
using boost::system::error_code;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Chat over Redis pubsub. To test, run this program from multiple
|
||||
// terminals and type messages to stdin.
|
||||
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
while (conn->will_reconnect()) {
|
||||
generic_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
while (conn->will_reconnect()) {
|
||||
// Subscribe to channels.
|
||||
co_await conn->async_exec(req, ignore, net::deferred);
|
||||
co_await conn->async_exec(req, ignore);
|
||||
|
||||
// Loop reading Redis push messages.
|
||||
for (generic_response resp;;) {
|
||||
error_code ec;
|
||||
co_await conn->async_receive(resp, redirect_error(use_awaitable, ec));
|
||||
for (error_code ec;;) {
|
||||
co_await conn->async_receive(redirect_error(use_awaitable, ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
std::cout
|
||||
<< resp.value().at(1).value
|
||||
<< " " << resp.value().at(2).value
|
||||
<< " " << resp.value().at(3).value
|
||||
<< std::endl;
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
|
||||
<< resp.value().at(3).value << std::endl;
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publishes stdin messages to a Redis channel.
|
||||
auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection> conn)
|
||||
-> awaitable<void>
|
||||
{
|
||||
for (std::string msg;;) {
|
||||
auto n = co_await net::async_read_until(*in, net::dynamic_buffer(msg, 1024), "\n");
|
||||
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, net::deferred);
|
||||
co_await conn->async_exec(req, ignore);
|
||||
msg.erase(0, n);
|
||||
}
|
||||
}
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
auto co_main(config cfg) -> awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
|
||||
|
||||
net::co_spawn(ex, receiver(conn), net::detached);
|
||||
net::co_spawn(ex, publisher(stream, conn), net::detached);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
co_spawn(ex, receiver(conn), detached);
|
||||
co_spawn(ex, publisher(stream, conn), detached);
|
||||
conn->async_run(cfg, consign(detached, conn));
|
||||
|
||||
signal_set sig_set{ex, SIGINT, SIGTERM};
|
||||
co_await sig_set.async_wait();
|
||||
@@ -89,11 +94,11 @@ auto co_main(config cfg) -> net::awaitable<void>
|
||||
stream->cancel();
|
||||
}
|
||||
|
||||
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
|
||||
auto co_main(config const&) -> net::awaitable<void>
|
||||
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
|
||||
auto co_main(config const&) -> awaitable<void>
|
||||
{
|
||||
std::cout << "Requires support for posix streams." << std::endl;
|
||||
co_return;
|
||||
}
|
||||
#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
145
example/cpp20_containers.cpp
Normal file
145
example/cpp20_containers.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/* 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/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::asio::awaitable;
|
||||
using boost::asio::detached;
|
||||
using boost::asio::consign;
|
||||
|
||||
template <class T>
|
||||
std::ostream& operator<<(std::ostream& os, std::optional<T> const& opt)
|
||||
{
|
||||
if (opt.has_value())
|
||||
std::cout << opt.value();
|
||||
else
|
||||
std::cout << "null";
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
void print(std::map<std::string, std::string> const& cont)
|
||||
{
|
||||
for (auto const& e : cont)
|
||||
std::cout << e.first << ": " << e.second << "\n";
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void print(std::vector<T> const& cont)
|
||||
{
|
||||
for (auto const& e : cont)
|
||||
std::cout << e << " ";
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
// Stores the content of some STL containers in Redis.
|
||||
auto store(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
std::vector<int> vec{1, 2, 3, 4, 5, 6};
|
||||
|
||||
std::map<std::string, std::string> map{
|
||||
{"key1", "value1"},
|
||||
{"key2", "value2"},
|
||||
{"key3", "value3"}
|
||||
};
|
||||
|
||||
request req;
|
||||
req.push_range("RPUSH", "rpush-key", vec);
|
||||
req.push_range("HSET", "hset-key", map);
|
||||
req.push("SET", "key", "value");
|
||||
|
||||
co_await conn->async_exec(req, ignore);
|
||||
}
|
||||
|
||||
auto hgetall(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
// A request contains multiple commands.
|
||||
request req;
|
||||
req.push("HGETALL", "hset-key");
|
||||
|
||||
// Responses as tuple elements.
|
||||
response<std::map<std::string, std::string>> resp;
|
||||
|
||||
// Executes the request and reads the response.
|
||||
co_await conn->async_exec(req, resp);
|
||||
|
||||
print(std::get<0>(resp).value());
|
||||
}
|
||||
|
||||
auto mget(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
// A request contains multiple commands.
|
||||
request req;
|
||||
req.push("MGET", "key", "non-existing-key");
|
||||
|
||||
// Responses as tuple elements.
|
||||
response<std::vector<std::optional<std::string>>> resp;
|
||||
|
||||
// Executes the request and reads the response.
|
||||
co_await conn->async_exec(req, resp);
|
||||
|
||||
print(std::get<0>(resp).value());
|
||||
}
|
||||
|
||||
// Retrieves in a transaction.
|
||||
auto transaction(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("MULTI");
|
||||
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
|
||||
req.push("HGETALL", "hset-key"); // Retrieves
|
||||
req.push("MGET", "key", "non-existing-key");
|
||||
req.push("EXEC");
|
||||
|
||||
response<
|
||||
ignore_t, // multi
|
||||
ignore_t, // lrange
|
||||
ignore_t, // hgetall
|
||||
ignore_t, // mget
|
||||
response<
|
||||
std::optional<std::vector<int>>,
|
||||
std::optional<std::map<std::string, std::string>>,
|
||||
std::optional<std::vector<std::optional<std::string>>>> // exec
|
||||
>
|
||||
resp;
|
||||
|
||||
co_await conn->async_exec(req, resp);
|
||||
|
||||
print(std::get<0>(std::get<4>(resp).value()).value().value());
|
||||
print(std::get<1>(std::get<4>(resp).value()).value().value());
|
||||
print(std::get<2>(std::get<4>(resp).value()).value().value());
|
||||
}
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
awaitable<void> co_main(config cfg)
|
||||
{
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
conn->async_run(cfg, consign(detached, conn));
|
||||
|
||||
co_await store(conn);
|
||||
co_await transaction(conn);
|
||||
co_await hgetall(conn);
|
||||
co_await mget(conn);
|
||||
conn->cancel();
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -5,19 +5,18 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
using tcp_socket = net::deferred_t::as_default_on_t<net::ip::tcp::socket>;
|
||||
using tcp_acceptor = net::deferred_t::as_default_on_t<net::ip::tcp::acceptor>;
|
||||
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
|
||||
namespace asio = boost::asio;
|
||||
using boost::asio::signal_set;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
@@ -25,16 +24,17 @@ using boost::system::error_code;
|
||||
using boost::redis::connection;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto echo_server_session(tcp_socket socket, std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr<connection> conn)
|
||||
-> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
response<std::string> resp;
|
||||
|
||||
for (std::string buffer;;) {
|
||||
auto n = co_await net::async_read_until(socket, net::dynamic_buffer(buffer, 1024), "\n");
|
||||
auto n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buffer, 1024), "\n");
|
||||
req.push("PING", buffer);
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
co_await net::async_write(socket, net::buffer(std::get<0>(resp).value()));
|
||||
co_await conn->async_exec(req, resp);
|
||||
co_await asio::async_write(socket, asio::buffer(std::get<0>(resp).value()));
|
||||
std::get<0>(resp).value().clear();
|
||||
req.clear();
|
||||
buffer.erase(0, n);
|
||||
@@ -42,29 +42,29 @@ auto echo_server_session(tcp_socket socket, std::shared_ptr<connection> conn) ->
|
||||
}
|
||||
|
||||
// Listens for tcp connections.
|
||||
auto listener(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto listener(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
try {
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555});
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
asio::ip::tcp::acceptor acc(ex, {asio::ip::tcp::v4(), 55555});
|
||||
for (;;)
|
||||
net::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), net::detached);
|
||||
asio::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), asio::detached);
|
||||
} catch (std::exception const& e) {
|
||||
std::clog << "Listener: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
net::co_spawn(ex, listener(conn), net::detached);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
asio::co_spawn(ex, listener(conn), asio::detached);
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
signal_set sig_set(ex, SIGINT, SIGTERM);
|
||||
co_await sig_set.async_wait();
|
||||
conn->cancel();
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -5,25 +5,26 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
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) -> net::awaitable<void>
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
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;
|
||||
@@ -33,10 +34,10 @@ auto co_main(config cfg) -> net::awaitable<void>
|
||||
response<std::string> resp;
|
||||
|
||||
// Executes the request.
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
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)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -5,28 +5,30 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
using boost::redis::logger;
|
||||
using boost::redis::connection;
|
||||
|
||||
auto verify_certificate(bool, net::ssl::verify_context&) -> bool
|
||||
auto verify_certificate(bool, asio::ssl::verify_context&) -> bool
|
||||
{
|
||||
std::cout << "set_verify_callback" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
cfg.use_ssl = true;
|
||||
cfg.username = "aedis";
|
||||
@@ -34,21 +36,22 @@ auto co_main(config cfg) -> net::awaitable<void>
|
||||
cfg.addr.host = "db.occase.de";
|
||||
cfg.addr.port = "6380";
|
||||
|
||||
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
asio::ssl::context ctx{asio::ssl::context::tlsv12_client};
|
||||
ctx.set_verify_mode(asio::ssl::verify_peer);
|
||||
ctx.set_verify_callback(verify_certificate);
|
||||
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor, std::move(ctx));
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
request req;
|
||||
req.push("PING");
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
conn->next_layer().set_verify_mode(net::ssl::verify_peer);
|
||||
conn->next_layer().set_verify_callback(verify_certificate);
|
||||
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -5,33 +5,35 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/describe.hpp>
|
||||
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <string>
|
||||
#include <boost/describe.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
#define BOOST_JSON_NO_LIB
|
||||
#define BOOST_CONTAINER_NO_LIB
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
|
||||
namespace net = boost::asio;
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace resp3 = boost::redis::resp3;
|
||||
using namespace boost::describe;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::resp3::node_view;
|
||||
|
||||
// Struct that will be stored in Redis using json serialization.
|
||||
// Struct that will be stored in Redis using json serialization.
|
||||
struct user {
|
||||
std::string name;
|
||||
std::string age;
|
||||
@@ -41,37 +43,40 @@ struct user {
|
||||
// The type must be described for serialization to work.
|
||||
BOOST_DESCRIBE_STRUCT(user, (), (name, age, country))
|
||||
|
||||
// Boost.Redis customization points (examples/json.hpp)
|
||||
// Boost.Redis customization points (example/json.hpp)
|
||||
void boost_redis_to_bulk(std::string& to, user const& u)
|
||||
{ boost::redis::resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u))); }
|
||||
|
||||
void boost_redis_from_bulk(user& u, std::string_view sv, boost::system::error_code&)
|
||||
{ u = boost::json::value_to<user>(boost::json::parse(sv)); }
|
||||
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u)));
|
||||
}
|
||||
|
||||
void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&)
|
||||
{
|
||||
u = boost::json::value_to<user>(boost::json::parse(node.value));
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
// user object that will be stored in Redis in json format.
|
||||
user const u{"Joao", "58", "Brazil"};
|
||||
|
||||
// Stores and retrieves in the same request.
|
||||
request req;
|
||||
req.push("SET", "json-key", u); // Stores in Redis.
|
||||
req.push("GET", "json-key"); // Retrieves from Redis.
|
||||
req.push("SET", "json-key", u); // Stores in Redis.
|
||||
req.push("GET", "json-key"); // Retrieves from Redis.
|
||||
|
||||
response<ignore_t, user> resp;
|
||||
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
// Prints the first ping
|
||||
std::cout
|
||||
<< "Name: " << std::get<1>(resp).value().name << "\n"
|
||||
<< "Age: " << std::get<1>(resp).value().age << "\n"
|
||||
<< "Country: " << std::get<1>(resp).value().country << "\n";
|
||||
std::cout << "Name: " << std::get<1>(resp).value().name << "\n"
|
||||
<< "Age: " << std::get<1>(resp).value().age << "\n"
|
||||
<< "Country: " << std::get<1>(resp).value().country << "\n";
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
// See the definition in person.proto. This header is automatically
|
||||
@@ -19,20 +20,21 @@
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
namespace asio = boost::asio;
|
||||
namespace resp3 = boost::redis::resp3;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::operation;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::resp3::node_view;
|
||||
|
||||
// The protobuf type described in examples/person.proto
|
||||
// The protobuf type described in example/person.proto
|
||||
using tutorial::person;
|
||||
|
||||
// Boost.Redis customization points (examples/protobuf.hpp)
|
||||
namespace tutorial
|
||||
{
|
||||
// Boost.Redis customization points (example/protobuf.hpp)
|
||||
namespace tutorial {
|
||||
|
||||
// Below I am using a Boost.Redis to indicate a protobuf error, this
|
||||
// is ok for an example, users however might want to define their own
|
||||
@@ -43,26 +45,26 @@ void boost_redis_to_bulk(std::string& to, person const& u)
|
||||
if (!u.SerializeToString(&tmp))
|
||||
throw boost::system::system_error(boost::redis::error::invalid_data_type);
|
||||
|
||||
boost::redis::resp3::boost_redis_to_bulk(to, tmp);
|
||||
resp3::boost_redis_to_bulk(to, tmp);
|
||||
}
|
||||
|
||||
void boost_redis_from_bulk(person& u, std::string_view sv, boost::system::error_code& ec)
|
||||
void boost_redis_from_bulk(person& u, node_view const& node, boost::system::error_code& ec)
|
||||
{
|
||||
std::string const tmp {sv};
|
||||
std::string const tmp{node.value};
|
||||
if (!u.ParseFromString(tmp))
|
||||
ec = boost::redis::error::invalid_data_type;
|
||||
}
|
||||
|
||||
} // tutorial
|
||||
} // namespace tutorial
|
||||
|
||||
using tutorial::boost_redis_to_bulk;
|
||||
using tutorial::boost_redis_from_bulk;
|
||||
|
||||
net::awaitable<void> co_main(config cfg)
|
||||
asio::awaitable<void> co_main(config cfg)
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
person p;
|
||||
p.set_name("Louis");
|
||||
@@ -76,13 +78,12 @@ net::awaitable<void> co_main(config cfg)
|
||||
response<ignore_t, person> resp;
|
||||
|
||||
// Sends the request and receives the response.
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout
|
||||
<< "Name: " << std::get<1>(resp).value().name() << "\n"
|
||||
<< "Age: " << std::get<1>(resp).value().id() << "\n"
|
||||
<< "Email: " << std::get<1>(resp).value().email() << "\n";
|
||||
std::cout << "Name: " << std::get<1>(resp).value().name() << "\n"
|
||||
<< "Age: " << std::get<1>(resp).value().id() << "\n"
|
||||
<< "Email: " << std::get<1>(resp).value().email() << "\n";
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -5,15 +5,17 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/asio/redirect_error.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 net = boost::asio;
|
||||
using endpoints = net::ip::tcp::resolver::results_type;
|
||||
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;
|
||||
@@ -21,19 +23,18 @@ using boost::redis::config;
|
||||
using boost::redis::address;
|
||||
using boost::redis::connection;
|
||||
|
||||
auto redir(boost::system::error_code& ec)
|
||||
{ return net::redirect_error(net::use_awaitable, ec); }
|
||||
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) -> net::awaitable<address>
|
||||
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 net::this_coro::executor);
|
||||
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) {
|
||||
@@ -43,33 +44,33 @@ auto resolve_master_address(std::vector<address> const& addresses) -> net::await
|
||||
// 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, {}, net::consign(net::detached, conn));
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
co_await conn->async_exec(req, resp, redir(ec));
|
||||
conn->cancel();
|
||||
conn->reset_stream();
|
||||
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{
|
||||
std::get<0>(resp).value().value().at(0),
|
||||
std::get<0>(resp).value().value().at(1)};
|
||||
}
|
||||
|
||||
co_return address{};
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
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
|
||||
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;
|
||||
std::clog << "Host: " << ep.host << "\n"
|
||||
<< "Port: " << ep.port << "\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
97
example/cpp20_streams.cpp
Normal file
97
example/cpp20_streams.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using boost::redis::config;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::operation;
|
||||
using boost::redis::request;
|
||||
using boost::redis::connection;
|
||||
using net::signal_set;
|
||||
|
||||
auto stream_reader(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
std::string redisStreamKey_;
|
||||
request req;
|
||||
generic_response resp;
|
||||
|
||||
std::string stream_id{"$"};
|
||||
std::string const field = "myfield";
|
||||
|
||||
for (;;) {
|
||||
req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id);
|
||||
co_await conn->async_exec(req, resp);
|
||||
|
||||
//std::cout << "Response: ";
|
||||
//for (auto i = 0UL; i < resp->size(); ++i) {
|
||||
// std::cout << resp->at(i).value << ", ";
|
||||
//}
|
||||
//std::cout << std::endl;
|
||||
|
||||
// The following approach was taken in order to be able to
|
||||
// deal with the responses, as generated by redis in the case
|
||||
// that there are multiple stream 'records' within a single
|
||||
// generic_response. The nesting and number of values in
|
||||
// resp.value() are different, depending on the contents
|
||||
// of the stream in redis. Uncomment the above commented-out
|
||||
// code for examples while running the XADD command.
|
||||
|
||||
std::size_t item_index = 0;
|
||||
while (item_index < std::size(resp.value())) {
|
||||
auto const& val = resp.value().at(item_index).value;
|
||||
|
||||
if (field.compare(val) == 0) {
|
||||
// We've hit a myfield field.
|
||||
// The streamId is located at item_index - 2
|
||||
// The payload is located at item_index + 1
|
||||
stream_id = resp.value().at(item_index - 2).value;
|
||||
std::cout << "StreamId: " << stream_id << ", "
|
||||
<< "MyField: " << resp.value().at(item_index + 1).value << std::endl;
|
||||
++item_index; // We can increase so we don't read this again
|
||||
}
|
||||
|
||||
++item_index;
|
||||
}
|
||||
|
||||
req.clear();
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Run this in another terminal:
|
||||
// redis-cli -r 100000 -i 0.0001 XADD "test-topic" "*" "myfield" "myfieldvalue1"
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
net::co_spawn(ex, stream_reader(conn), net::detached);
|
||||
|
||||
// Disable health checks.
|
||||
cfg.health_check_interval = std::chrono::seconds::zero();
|
||||
conn->async_run(cfg, net::consign(net::detached, conn));
|
||||
|
||||
signal_set sig_set(ex, SIGINT, SIGTERM);
|
||||
co_await sig_set.async_wait();
|
||||
conn->cancel();
|
||||
}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -6,28 +6,31 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
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::config;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::error;
|
||||
using boost::system::error_code;
|
||||
using boost::redis::connection;
|
||||
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
|
||||
using asio::signal_set;
|
||||
|
||||
/* This example will subscribe and read pushes indefinitely.
|
||||
*
|
||||
@@ -46,40 +49,45 @@ using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
|
||||
*/
|
||||
|
||||
// Receives server pushes.
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
while (conn->will_reconnect()) {
|
||||
|
||||
// Reconnect to channels.
|
||||
co_await conn->async_exec(req, ignore, net::deferred);
|
||||
// Reconnect to the channels.
|
||||
co_await conn->async_exec(req, ignore);
|
||||
|
||||
// Loop reading Redis pushs messages.
|
||||
for (generic_response resp;;) {
|
||||
error_code ec;
|
||||
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
|
||||
for (error_code ec;;) {
|
||||
// First tries to read any buffered pushes.
|
||||
conn->receive(ec);
|
||||
if (ec == error::sync_receive_push_failed) {
|
||||
ec = {};
|
||||
co_await conn->async_receive(asio::redirect_error(asio::use_awaitable, ec));
|
||||
}
|
||||
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
std::cout
|
||||
<< resp.value().at(1).value
|
||||
<< " " << resp.value().at(2).value
|
||||
<< " " << resp.value().at(3).value
|
||||
<< std::endl;
|
||||
resp.value().clear();
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
|
||||
<< resp.value().at(3).value << std::endl;
|
||||
|
||||
consume_one(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
net::co_spawn(ex, receiver(conn), net::detached);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
asio::co_spawn(ex, receiver(conn), asio::detached);
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
signal_set sig_set(ex, SIGINT, SIGTERM);
|
||||
co_await sig_set.async_wait();
|
||||
@@ -87,4 +95,4 @@ auto co_main(config cfg) -> net::awaitable<void>
|
||||
conn->cancel();
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
60
example/cpp20_unix_sockets.cpp
Normal file
60
example/cpp20_unix_sockets.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/this_coro.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
using boost::redis::logger;
|
||||
using boost::redis::connection;
|
||||
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
// If unix_socket is set to a non-empty string, UNIX domain sockets will be used
|
||||
// instead of TCP. Set this value to the path where your server is listening.
|
||||
// UNIX domain socket connections work in the same way as TCP connections.
|
||||
cfg.unix_socket = "/tmp/redis-socks/redis.sock";
|
||||
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
request req;
|
||||
req.push("PING");
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
auto co_main(config) -> asio::awaitable<void>
|
||||
{
|
||||
std::cout << "Sorry, your system does not support UNIX domain sockets\n";
|
||||
co_return;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -4,22 +4,24 @@
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace net = boost::asio;
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::config;
|
||||
using boost::redis::logger;
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
extern net::awaitable<void> co_main(config);
|
||||
extern asio::awaitable<void> co_main(config);
|
||||
|
||||
auto main(int argc, char * argv[]) -> int
|
||||
auto main(int argc, char* argv[]) -> int
|
||||
{
|
||||
try {
|
||||
config cfg;
|
||||
@@ -29,8 +31,8 @@ auto main(int argc, char * argv[]) -> int
|
||||
cfg.addr.port = argv[2];
|
||||
}
|
||||
|
||||
net::io_context ioc;
|
||||
net::co_spawn(ioc, std::move(co_main(cfg)), [](std::exception_ptr p) {
|
||||
asio::io_context ioc;
|
||||
asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) {
|
||||
if (p)
|
||||
std::rethrow_exception(p);
|
||||
});
|
||||
@@ -42,7 +44,7 @@ auto main(int argc, char * argv[]) -> int
|
||||
}
|
||||
}
|
||||
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
auto main() -> int
|
||||
{
|
||||
@@ -50,4 +52,4 @@ auto main() -> int
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,16 +7,17 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/use_future.hpp>
|
||||
#include <thread>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace boost::redis
|
||||
{
|
||||
namespace boost::redis {
|
||||
|
||||
class sync_connection {
|
||||
public:
|
||||
@@ -25,33 +26,32 @@ public:
|
||||
, conn_{std::make_shared<connection>(ioc_)}
|
||||
{ }
|
||||
|
||||
~sync_connection()
|
||||
{
|
||||
thread_.join();
|
||||
}
|
||||
~sync_connection() { thread_.join(); }
|
||||
|
||||
void run(config cfg)
|
||||
{
|
||||
// Starts a thread that will can io_context::run on which the
|
||||
// connection will run.
|
||||
thread_ = std::thread{[this, cfg]() {
|
||||
conn_->async_run(cfg, {}, asio::detached);
|
||||
conn_->async_run(cfg, asio::detached);
|
||||
ioc_.run();
|
||||
}};
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
asio::dispatch(ioc_, [this]() { conn_->cancel(); });
|
||||
asio::dispatch(ioc_, [this]() {
|
||||
conn_->cancel();
|
||||
});
|
||||
}
|
||||
|
||||
template <class Response>
|
||||
auto exec(request const& req, Response& resp)
|
||||
{
|
||||
asio::dispatch(
|
||||
conn_->get_executor(),
|
||||
asio::deferred([this, &req, &resp]() { return conn_->async_exec(req, resp, asio::deferred); }))
|
||||
(asio::use_future).get();
|
||||
asio::dispatch(conn_->get_executor(), asio::deferred([this, &req, &resp]() {
|
||||
return conn_->async_exec(req, resp, asio::deferred);
|
||||
}))(asio::use_future)
|
||||
.get();
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -60,4 +60,4 @@ private:
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace boost::redis
|
||||
@@ -1,102 +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/deferred.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace net = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
|
||||
void print(std::map<std::string, std::string> const& cont)
|
||||
{
|
||||
for (auto const& e: cont)
|
||||
std::cout << e.first << ": " << e.second << "\n";
|
||||
}
|
||||
|
||||
void print(std::vector<int> const& cont)
|
||||
{
|
||||
for (auto const& e: cont) std::cout << e << " ";
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
// Stores the content of some STL containers in Redis.
|
||||
auto store(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
std::vector<int> vec
|
||||
{1, 2, 3, 4, 5, 6};
|
||||
|
||||
std::map<std::string, std::string> map
|
||||
{{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
|
||||
|
||||
request req;
|
||||
req.push_range("RPUSH", "rpush-key", vec);
|
||||
req.push_range("HSET", "hset-key", map);
|
||||
|
||||
co_await conn->async_exec(req, ignore, net::deferred);
|
||||
}
|
||||
|
||||
auto hgetall(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
// A request contains multiple commands.
|
||||
request req;
|
||||
req.push("HGETALL", "hset-key");
|
||||
|
||||
// Responses as tuple elements.
|
||||
response<std::map<std::string, std::string>> resp;
|
||||
|
||||
// Executes the request and reads the response.
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
|
||||
print(std::get<0>(resp).value());
|
||||
}
|
||||
|
||||
// Retrieves in a transaction.
|
||||
auto transaction(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("MULTI");
|
||||
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
|
||||
req.push("HGETALL", "hset-key"); // Retrieves
|
||||
req.push("EXEC");
|
||||
|
||||
response<
|
||||
ignore_t, // multi
|
||||
ignore_t, // lrange
|
||||
ignore_t, // hgetall
|
||||
response<std::optional<std::vector<int>>, std::optional<std::map<std::string, std::string>>> // exec
|
||||
> resp;
|
||||
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
|
||||
print(std::get<0>(std::get<3>(resp).value()).value().value());
|
||||
print(std::get<1>(std::get<3>(resp).value()).value().value());
|
||||
}
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
net::awaitable<void> co_main(config cfg)
|
||||
{
|
||||
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
|
||||
co_await store(conn);
|
||||
co_await transaction(conn);
|
||||
co_await hgetall(conn);
|
||||
conn->cancel();
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -1,98 +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/deferred.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using boost::redis::config;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::operation;
|
||||
using boost::redis::request;
|
||||
using boost::redis::connection;
|
||||
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
|
||||
|
||||
auto stream_reader(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
{
|
||||
std::string redisStreamKey_;
|
||||
request req;
|
||||
generic_response resp;
|
||||
|
||||
std::string stream_id{"$"};
|
||||
std::string const field = "myfield";
|
||||
|
||||
for (;;) {
|
||||
req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id);
|
||||
co_await conn->async_exec(req, resp, net::deferred);
|
||||
|
||||
//std::cout << "Response: ";
|
||||
//for (auto i = 0UL; i < resp->size(); ++i) {
|
||||
// std::cout << resp->at(i).value << ", ";
|
||||
//}
|
||||
//std::cout << std::endl;
|
||||
|
||||
// The following approach was taken in order to be able to
|
||||
// deal with the responses, as generated by redis in the case
|
||||
// that there are multiple stream 'records' within a single
|
||||
// generic_response. The nesting and number of values in
|
||||
// resp.value() are different, depending on the contents
|
||||
// of the stream in redis. Uncomment the above commented-out
|
||||
// code for examples while running the XADD command.
|
||||
|
||||
std::size_t item_index = 0;
|
||||
while (item_index < std::size(resp.value())) {
|
||||
auto const& val = resp.value().at(item_index).value;
|
||||
|
||||
if (field.compare(val) == 0) {
|
||||
// We've hit a myfield field.
|
||||
// The streamId is located at item_index - 2
|
||||
// The payload is located at item_index + 1
|
||||
stream_id = resp.value().at(item_index - 2).value;
|
||||
std::cout
|
||||
<< "StreamId: " << stream_id << ", "
|
||||
<< "MyField: " << resp.value().at(item_index + 1).value
|
||||
<< std::endl;
|
||||
++item_index; // We can increase so we don't read this again
|
||||
}
|
||||
|
||||
++item_index;
|
||||
}
|
||||
|
||||
req.clear();
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Run this in another terminal:
|
||||
// redis-cli -r 100000 -i 0.0001 XADD "test-topic" "*" "myfield" "myfieldvalue1"
|
||||
auto co_main(config cfg) -> net::awaitable<void>
|
||||
{
|
||||
auto ex = co_await net::this_coro::executor;
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
net::co_spawn(ex, stream_reader(conn), net::detached);
|
||||
|
||||
// Disable health checks.
|
||||
cfg.health_check_interval = std::chrono::seconds::zero();
|
||||
conn->async_run(cfg, {}, net::consign(net::detached, conn));
|
||||
|
||||
signal_set sig_set(ex, SIGINT, SIGTERM);
|
||||
co_await sig_set.async_wait();
|
||||
conn->cancel();
|
||||
}
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -8,12 +8,12 @@
|
||||
#define BOOST_REDIS_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
/** @defgroup high-level-api Reference
|
||||
*
|
||||
@@ -25,4 +25,4 @@
|
||||
* This page contains the documentation of the Aedis low-level API.
|
||||
*/
|
||||
|
||||
#endif // BOOST_REDIS_HPP
|
||||
#endif // BOOST_REDIS_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,74 +7,40 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_ADAPT_HPP
|
||||
#define BOOST_REDIS_ADAPTER_ADAPT_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
#include <boost/redis/adapter/detail/result_traits.hpp>
|
||||
#include <boost/redis/adapter/detail/response_traits.hpp>
|
||||
#include <boost/mp11.hpp>
|
||||
#include <boost/system.hpp>
|
||||
#include <boost/redis/adapter/detail/result_traits.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
|
||||
#include <tuple>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace boost::redis::adapter
|
||||
{
|
||||
namespace boost::redis::adapter {
|
||||
|
||||
/** @brief Adapts a type to be used as a response.
|
||||
*
|
||||
* The type T must be either
|
||||
*
|
||||
* 1. a response<T1, T2, T3, ...> or
|
||||
* 2. std::vector<node<String>>
|
||||
* @li a `response<T1, T2, T3, ...>`
|
||||
* @li `std::vector<node<String>>`
|
||||
*
|
||||
* The types T1, T2, etc can be any STL container, any integer type
|
||||
* and `std::string`.
|
||||
*
|
||||
* @param t Tuple containing the responses.
|
||||
* @tparam T The response type.
|
||||
*/
|
||||
template<class T>
|
||||
template <class T>
|
||||
auto boost_redis_adapt(T& t) noexcept
|
||||
{
|
||||
return detail::response_traits<T>::adapt(t);
|
||||
}
|
||||
|
||||
/** @brief Adapts user data to read operations.
|
||||
* @ingroup low-level-api
|
||||
/** @brief Adapts a type to be used as the response to an individual command.
|
||||
*
|
||||
* STL containers, \c resp3::response and built-in types are supported and
|
||||
* can be used in conjunction with \c std::optional<T>.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* @code
|
||||
* std::unordered_map<std::string, std::string> cont;
|
||||
* co_await async_read(socket, buffer, adapt(cont));
|
||||
* @endcode
|
||||
*
|
||||
* For a transaction
|
||||
*
|
||||
* @code
|
||||
* sr.push(command::multi);
|
||||
* sr.push(command::ping, ...);
|
||||
* sr.push(command::incr, ...);
|
||||
* sr.push_range(command::rpush, ...);
|
||||
* sr.push(command::lrange, ...);
|
||||
* sr.push(command::incr, ...);
|
||||
* sr.push(command::exec);
|
||||
*
|
||||
* co_await async_write(socket, buffer(request));
|
||||
*
|
||||
* // Reads the response to a transaction
|
||||
* resp3::response<std::string, int, int, std::vector<std::string>, int> execs;
|
||||
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs));
|
||||
* @endcode
|
||||
* It can be used with low-level APIs, like @ref boost::redis::resp3::parser.
|
||||
*/
|
||||
template<class T>
|
||||
template <class T>
|
||||
auto adapt2(T& t = redis::ignore) noexcept
|
||||
{ return detail::result_traits<T>::adapt(t); }
|
||||
{
|
||||
return detail::result_traits<T>::adapt(t);
|
||||
}
|
||||
|
||||
} // boost::redis::adapter
|
||||
} // namespace boost::redis::adapter
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_ADAPT_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_ADAPT_HPP
|
||||
|
||||
114
include/boost/redis/adapter/any_adapter.hpp
Normal file
114
include/boost/redis/adapter/any_adapter.hpp
Normal file
@@ -0,0 +1,114 @@
|
||||
/* 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_ANY_ADAPTER_HPP
|
||||
#define BOOST_REDIS_ANY_ADAPTER_HPP
|
||||
|
||||
#include <boost/redis/adapter/adapt.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
/** @brief A type-erased reference to a response.
|
||||
*
|
||||
* A type-erased response adapter. It can be executed using @ref connection::async_exec.
|
||||
* Using this type instead of raw response references enables separate compilation.
|
||||
*
|
||||
* Given a response object `resp` that can be passed to `async_exec`, the following two
|
||||
* statements have the same effect:
|
||||
*
|
||||
* @code
|
||||
* co_await conn.async_exec(req, resp);
|
||||
* co_await conn.async_exec(req, any_response(resp));
|
||||
* @endcode
|
||||
*/
|
||||
class any_adapter {
|
||||
public:
|
||||
/** @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
|
||||
};
|
||||
|
||||
/// 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;
|
||||
return [adapter2 = boost_redis_adapt(resp)](
|
||||
any_adapter::parse_event ev,
|
||||
resp3::node_view const& nd,
|
||||
system::error_code& ec) mutable {
|
||||
switch (ev) {
|
||||
case parse_event::init: adapter2.on_init(); break;
|
||||
case parse_event::node: adapter2.on_node(nd, ec); break;
|
||||
case parse_event::done: adapter2.on_done(); break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 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.
|
||||
*
|
||||
* Creates a type-erased response adapter from `resp` by calling
|
||||
* `boost_redis_adapt`. `T` must be a valid Redis response type.
|
||||
* Any type passed to @ref connection::async_exec qualifies.
|
||||
*
|
||||
* This object stores a reference to `resp`, which must be kept alive
|
||||
* while `*this` is being used.
|
||||
*/
|
||||
template <class T, class = std::enable_if_t<!std::is_same_v<T, any_adapter>>>
|
||||
explicit any_adapter(T& resp)
|
||||
: impl_(create_impl(resp))
|
||||
{ }
|
||||
|
||||
/// Calls the implementation with the arguments `impl_(parse_event::init, ...);`
|
||||
void on_init()
|
||||
{
|
||||
system::error_code ec;
|
||||
impl_(parse_event::init, {}, ec);
|
||||
};
|
||||
|
||||
/// Calls the implementation with the arguments `impl_(parse_event::done, ...);`
|
||||
void on_done()
|
||||
{
|
||||
system::error_code ec;
|
||||
impl_(parse_event::done, {}, ec);
|
||||
};
|
||||
|
||||
/// Calls the implementation with the arguments `impl_(parse_event::node, ...);`
|
||||
void on_node(resp3::node_view const& nd, system::error_code& ec)
|
||||
{
|
||||
impl_(parse_event::node, nd, ec);
|
||||
};
|
||||
|
||||
private:
|
||||
impl_t impl_;
|
||||
};
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,80 +7,133 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_ADAPTERS_HPP
|
||||
#define BOOST_REDIS_ADAPTER_ADAPTERS_HPP
|
||||
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
#include <forward_list>
|
||||
#include <system_error>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <list>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <charconv>
|
||||
#include <deque>
|
||||
#include <forward_list>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// See https://stackoverflow.com/a/31658120/1077832
|
||||
#include<ciso646>
|
||||
#ifdef _LIBCPP_VERSION
|
||||
#else
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
namespace boost::redis::adapter::detail
|
||||
{
|
||||
namespace boost::redis::adapter::detail {
|
||||
|
||||
// Serialization.
|
||||
// Exclude bools, char and charXY_t types
|
||||
template <class T> struct is_integral_number : std::is_integral<T> { };
|
||||
template <> struct is_integral_number<bool> : std::false_type { };
|
||||
template <> struct is_integral_number<char> : std::false_type { };
|
||||
template <> struct is_integral_number<char16_t> : std::false_type { };
|
||||
template <> struct is_integral_number<char32_t> : std::false_type { };
|
||||
template <> struct is_integral_number<wchar_t> : std::false_type { };
|
||||
#ifdef __cpp_char8_t
|
||||
template <> struct is_integral_number<char8_t> : std::false_type { };
|
||||
#endif
|
||||
|
||||
template <class T, bool = is_integral_number<T>::value>
|
||||
struct converter;
|
||||
|
||||
template <class T>
|
||||
auto boost_redis_from_bulk(T& i, std::string_view sv, system::error_code& ec) -> typename std::enable_if<std::is_integral<T>::value, void>::type
|
||||
{
|
||||
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), i);
|
||||
if (res.ec != std::errc())
|
||||
ec = redis::error::not_a_number;
|
||||
}
|
||||
struct converter<T, true> {
|
||||
template <class String>
|
||||
static void apply(T& i, resp3::basic_node<String> const& node, system::error_code& ec)
|
||||
{
|
||||
auto const res = std::from_chars(node.value.data(), node.value.data() + node.value.size(), i);
|
||||
if (res.ec != std::errc())
|
||||
ec = redis::error::not_a_number;
|
||||
}
|
||||
};
|
||||
|
||||
inline
|
||||
void boost_redis_from_bulk(bool& t, std::string_view sv, system::error_code&)
|
||||
{
|
||||
t = *sv.data() == 't';
|
||||
}
|
||||
template <>
|
||||
struct converter<bool, false> {
|
||||
template <class String>
|
||||
static void apply(bool& t, resp3::basic_node<String> const& node, system::error_code&)
|
||||
{
|
||||
t = *node.value.data() == 't';
|
||||
}
|
||||
};
|
||||
|
||||
inline
|
||||
void boost_redis_from_bulk(double& d, std::string_view sv, system::error_code& ec)
|
||||
{
|
||||
template <>
|
||||
struct converter<double, false> {
|
||||
template <class String>
|
||||
static void apply(double& d, resp3::basic_node<String> const& node, system::error_code& ec)
|
||||
{
|
||||
#ifdef _LIBCPP_VERSION
|
||||
// The string in sv is not null terminated and we also don't know
|
||||
// if there is enough space at the end for a null char. The easiest
|
||||
// thing to do is to create a temporary.
|
||||
std::string const tmp{sv.data(), sv.data() + std::size(sv)};
|
||||
char* end{};
|
||||
d = std::strtod(tmp.data(), &end);
|
||||
if (d == HUGE_VAL || d == 0)
|
||||
ec = redis::error::not_a_double;
|
||||
// The string in node.value is not null terminated and we also
|
||||
// don't know if there is enough space at the end for a null
|
||||
// char. The easiest thing to do is to create a temporary.
|
||||
std::string const tmp{node.value.data(), node.value.data() + node.value.size()};
|
||||
char* end{};
|
||||
d = std::strtod(tmp.data(), &end);
|
||||
if (d == HUGE_VAL || d == 0)
|
||||
ec = redis::error::not_a_double;
|
||||
#else
|
||||
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), d);
|
||||
if (res.ec != std::errc())
|
||||
ec = redis::error::not_a_double;
|
||||
#endif // _LIBCPP_VERSION
|
||||
}
|
||||
auto const res = std::from_chars(node.value.data(), node.value.data() + node.value.size(), d);
|
||||
if (res.ec != std::errc())
|
||||
ec = redis::error::not_a_double;
|
||||
#endif // _LIBCPP_VERSION
|
||||
}
|
||||
};
|
||||
|
||||
template <class CharT, class Traits, class Allocator>
|
||||
void
|
||||
boost_redis_from_bulk(
|
||||
std::basic_string<CharT, Traits, Allocator>& s,
|
||||
std::string_view sv,
|
||||
system::error_code&)
|
||||
struct converter<std::basic_string<CharT, Traits, Allocator>, false> {
|
||||
template <class String>
|
||||
static void apply(
|
||||
std::basic_string<CharT, Traits, Allocator>& s,
|
||||
resp3::basic_node<String> const& node,
|
||||
system::error_code&)
|
||||
{
|
||||
s.append(node.value.data(), node.value.size());
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct from_bulk_impl {
|
||||
template <class String>
|
||||
static void apply(T& t, resp3::basic_node<String> const& node, system::error_code& ec)
|
||||
{
|
||||
converter<T>::apply(t, node, ec);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct from_bulk_impl<std::optional<T>> {
|
||||
template <class String>
|
||||
static void apply(
|
||||
std::optional<T>& op,
|
||||
resp3::basic_node<String> const& node,
|
||||
system::error_code& ec)
|
||||
{
|
||||
if (node.data_type != resp3::type::null) {
|
||||
op.emplace(T{});
|
||||
converter<T>::apply(op.value(), node, ec);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class T, class String>
|
||||
void boost_redis_from_bulk(T& t, resp3::basic_node<String> const& node, system::error_code& ec)
|
||||
{
|
||||
s.append(sv.data(), sv.size());
|
||||
from_bulk_impl<T>::apply(t, node, ec);
|
||||
}
|
||||
|
||||
//================================================
|
||||
@@ -91,17 +144,32 @@ private:
|
||||
Result* result_;
|
||||
|
||||
public:
|
||||
explicit general_aggregate(Result* c = nullptr): result_(c) {}
|
||||
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code&)
|
||||
explicit general_aggregate(Result* c = nullptr)
|
||||
: result_(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(!!result_, "Unexpected null pointer");
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::blob_error:
|
||||
case resp3::type::simple_error:
|
||||
*result_ = error{nd.data_type, std::string{std::cbegin(nd.value), std::cend(nd.value)}};
|
||||
*result_ = error{
|
||||
nd.data_type,
|
||||
std::string{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
result_->value().push_back({nd.data_type, nd.aggregate_size, nd.depth, std::string{std::cbegin(nd.value), std::cend(nd.value)}});
|
||||
result_->value().push_back({
|
||||
nd.data_type,
|
||||
nd.aggregate_size,
|
||||
nd.depth,
|
||||
std::string{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -112,15 +180,24 @@ private:
|
||||
Node* result_;
|
||||
|
||||
public:
|
||||
explicit general_simple(Node* t = nullptr) : result_(t) {}
|
||||
explicit general_simple(Node* t = nullptr)
|
||||
: result_(t)
|
||||
{ }
|
||||
|
||||
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code&)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::blob_error:
|
||||
case resp3::type::simple_error:
|
||||
*result_ = error{nd.data_type, std::string{std::cbegin(nd.value), std::cend(nd.value)}};
|
||||
*result_ = error{
|
||||
nd.data_type,
|
||||
std::string{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
result_->value().data_type = nd.data_type;
|
||||
@@ -134,20 +211,20 @@ public:
|
||||
template <class Result>
|
||||
class simple_impl {
|
||||
public:
|
||||
void on_value_available(Result&) {}
|
||||
void on_value_available(Result&) { }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& n,
|
||||
system::error_code& ec)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
void on_node(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
|
||||
{
|
||||
if (is_aggregate(n.data_type)) {
|
||||
if (is_aggregate(node.data_type)) {
|
||||
ec = redis::error::expects_resp3_simple_type;
|
||||
return;
|
||||
}
|
||||
|
||||
boost_redis_from_bulk(result, n.value, ec);
|
||||
boost_redis_from_bulk(result, node, ec);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,14 +234,13 @@ private:
|
||||
typename Result::iterator hint_;
|
||||
|
||||
public:
|
||||
void on_value_available(Result& result)
|
||||
{ hint_ = std::end(result); }
|
||||
void on_value_available(Result& result) { hint_ = std::end(result); }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
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)
|
||||
@@ -175,12 +251,12 @@ public:
|
||||
BOOST_ASSERT(nd.aggregate_size == 1);
|
||||
|
||||
if (nd.depth < 1) {
|
||||
ec = redis::error::expects_resp3_set;
|
||||
return;
|
||||
ec = redis::error::expects_resp3_set;
|
||||
return;
|
||||
}
|
||||
|
||||
typename Result::key_type obj;
|
||||
boost_redis_from_bulk(obj, nd.value, ec);
|
||||
boost_redis_from_bulk(obj, nd, ec);
|
||||
hint_ = result.insert(hint_, std::move(obj));
|
||||
}
|
||||
};
|
||||
@@ -192,35 +268,34 @@ private:
|
||||
bool on_key_ = true;
|
||||
|
||||
public:
|
||||
void on_value_available(Result& result)
|
||||
{ current_ = std::end(result); }
|
||||
void on_value_available(Result& result) { current_ = std::end(result); }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
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)
|
||||
ec = redis::error::expects_resp3_map;
|
||||
ec = redis::error::expects_resp3_map;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_ASSERT(nd.aggregate_size == 1);
|
||||
|
||||
if (nd.depth < 1) {
|
||||
ec = redis::error::expects_resp3_map;
|
||||
return;
|
||||
ec = redis::error::expects_resp3_map;
|
||||
return;
|
||||
}
|
||||
|
||||
if (on_key_) {
|
||||
typename Result::key_type obj;
|
||||
boost_redis_from_bulk(obj, nd.value, ec);
|
||||
boost_redis_from_bulk(obj, nd, ec);
|
||||
current_ = result.insert(current_, {std::move(obj), {}});
|
||||
} else {
|
||||
typename Result::mapped_type obj;
|
||||
boost_redis_from_bulk(obj, nd.value, ec);
|
||||
boost_redis_from_bulk(obj, nd, ec);
|
||||
current_->second = std::move(obj);
|
||||
}
|
||||
|
||||
@@ -231,20 +306,20 @@ public:
|
||||
template <class Result>
|
||||
class vector_impl {
|
||||
public:
|
||||
void on_value_available(Result& ) { }
|
||||
void on_value_available(Result&) { }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
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);
|
||||
result.reserve(result.size() + m * nd.aggregate_size);
|
||||
} else {
|
||||
result.push_back({});
|
||||
boost_redis_from_bulk(result.back(), nd.value, ec);
|
||||
boost_redis_from_bulk(result.back(), nd, ec);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -255,16 +330,16 @@ private:
|
||||
int i_ = -1;
|
||||
|
||||
public:
|
||||
void on_value_available(Result& ) { }
|
||||
void on_value_available(Result&) { }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
template <class String>
|
||||
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
|
||||
{
|
||||
if (is_aggregate(nd.data_type)) {
|
||||
if (i_ != -1) {
|
||||
if (i_ != -1) {
|
||||
ec = redis::error::nested_aggregate_not_supported;
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +355,7 @@ public:
|
||||
}
|
||||
|
||||
BOOST_ASSERT(nd.aggregate_size == 1);
|
||||
boost_redis_from_bulk(result.at(i_), nd.value, ec);
|
||||
boost_redis_from_bulk(result.at(i_), nd, ec);
|
||||
}
|
||||
|
||||
++i_;
|
||||
@@ -289,24 +364,23 @@ public:
|
||||
|
||||
template <class Result>
|
||||
struct list_impl {
|
||||
void on_value_available(Result&) { }
|
||||
|
||||
void on_value_available(Result& ) { }
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
|
||||
void
|
||||
operator()(
|
||||
Result& result,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
template <class String>
|
||||
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);
|
||||
if (nd.depth < 1) {
|
||||
ec = redis::error::expects_resp3_aggregate;
|
||||
return;
|
||||
}
|
||||
BOOST_ASSERT(nd.aggregate_size == 1);
|
||||
if (nd.depth < 1) {
|
||||
ec = redis::error::expects_resp3_aggregate;
|
||||
return;
|
||||
}
|
||||
|
||||
result.push_back({});
|
||||
boost_redis_from_bulk(result.back(), nd.value, ec);
|
||||
result.push_back({});
|
||||
boost_redis_from_bulk(result.back(), nd, ec);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -314,94 +388,127 @@ struct list_impl {
|
||||
//---------------------------------------------------
|
||||
|
||||
template <class T>
|
||||
struct impl_map { using type = simple_impl<T>; };
|
||||
struct impl_map {
|
||||
using type = simple_impl<T>;
|
||||
};
|
||||
|
||||
template <class Key, class Compare, class Allocator>
|
||||
struct impl_map<std::set<Key, Compare, Allocator>> { using type = set_impl<std::set<Key, Compare, Allocator>>; };
|
||||
struct impl_map<std::set<Key, Compare, Allocator>> {
|
||||
using type = set_impl<std::set<Key, Compare, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class Compare, class Allocator>
|
||||
struct impl_map<std::multiset<Key, Compare, Allocator>> { using type = set_impl<std::multiset<Key, Compare, Allocator>>; };
|
||||
struct impl_map<std::multiset<Key, Compare, Allocator>> {
|
||||
using type = set_impl<std::multiset<Key, Compare, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class Hash, class KeyEqual, class Allocator>
|
||||
struct impl_map<std::unordered_set<Key, Hash, KeyEqual, Allocator>> { using type = set_impl<std::unordered_set<Key, Hash, KeyEqual, Allocator>>; };
|
||||
struct impl_map<std::unordered_set<Key, Hash, KeyEqual, Allocator>> {
|
||||
using type = set_impl<std::unordered_set<Key, Hash, KeyEqual, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class Hash, class KeyEqual, class Allocator>
|
||||
struct impl_map<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>> { using type = set_impl<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>>; };
|
||||
struct impl_map<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>> {
|
||||
using type = set_impl<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class T, class Compare, class Allocator>
|
||||
struct impl_map<std::map<Key, T, Compare, Allocator>> { using type = map_impl<std::map<Key, T, Compare, Allocator>>; };
|
||||
struct impl_map<std::map<Key, T, Compare, Allocator>> {
|
||||
using type = map_impl<std::map<Key, T, Compare, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class T, class Compare, class Allocator>
|
||||
struct impl_map<std::multimap<Key, T, Compare, Allocator>> { using type = map_impl<std::multimap<Key, T, Compare, Allocator>>; };
|
||||
struct impl_map<std::multimap<Key, T, Compare, Allocator>> {
|
||||
using type = map_impl<std::multimap<Key, T, Compare, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class Hash, class KeyEqual, class Allocator>
|
||||
struct impl_map<std::unordered_map<Key, Hash, KeyEqual, Allocator>> { using type = map_impl<std::unordered_map<Key, Hash, KeyEqual, Allocator>>; };
|
||||
struct impl_map<std::unordered_map<Key, Hash, KeyEqual, Allocator>> {
|
||||
using type = map_impl<std::unordered_map<Key, Hash, KeyEqual, Allocator>>;
|
||||
};
|
||||
|
||||
template <class Key, class Hash, class KeyEqual, class Allocator>
|
||||
struct impl_map<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>> { using type = map_impl<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>>; };
|
||||
struct impl_map<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>> {
|
||||
using type = map_impl<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>>;
|
||||
};
|
||||
|
||||
template <class T, class Allocator>
|
||||
struct impl_map<std::vector<T, Allocator>> { using type = vector_impl<std::vector<T, Allocator>>; };
|
||||
struct impl_map<std::vector<T, Allocator>> {
|
||||
using type = vector_impl<std::vector<T, Allocator>>;
|
||||
};
|
||||
|
||||
template <class T, std::size_t N>
|
||||
struct impl_map<std::array<T, N>> { using type = array_impl<std::array<T, N>>; };
|
||||
struct impl_map<std::array<T, N>> {
|
||||
using type = array_impl<std::array<T, N>>;
|
||||
};
|
||||
|
||||
template <class T, class Allocator>
|
||||
struct impl_map<std::list<T, Allocator>> { using type = list_impl<std::list<T, Allocator>>; };
|
||||
struct impl_map<std::list<T, Allocator>> {
|
||||
using type = list_impl<std::list<T, Allocator>>;
|
||||
};
|
||||
|
||||
template <class T, class Allocator>
|
||||
struct impl_map<std::deque<T, Allocator>> { using type = list_impl<std::deque<T, Allocator>>; };
|
||||
struct impl_map<std::deque<T, Allocator>> {
|
||||
using type = list_impl<std::deque<T, Allocator>>;
|
||||
};
|
||||
|
||||
//---------------------------------------------------
|
||||
|
||||
template <class>
|
||||
class wrapper;
|
||||
|
||||
template <class Result>
|
||||
class wrapper<result<Result>> {
|
||||
template <class T>
|
||||
class wrapper<result<T>> {
|
||||
public:
|
||||
using response_type = result<Result>;
|
||||
using response_type = result<T>;
|
||||
|
||||
private:
|
||||
response_type* result_;
|
||||
typename impl_map<Result>::type impl_;
|
||||
typename impl_map<T>::type impl_;
|
||||
bool called_once_ = false;
|
||||
|
||||
bool set_if_resp3_error(resp3::basic_node<std::string_view> const& nd) noexcept
|
||||
template <class String>
|
||||
bool set_if_resp3_error(resp3::basic_node<String> const& nd) noexcept
|
||||
{
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::null:
|
||||
case resp3::type::simple_error:
|
||||
case resp3::type::blob_error:
|
||||
*result_ = error{nd.data_type, {std::cbegin(nd.value), std::cend(nd.value)}};
|
||||
*result_ = error{
|
||||
nd.data_type,
|
||||
{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
};
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit wrapper(response_type* t = nullptr) : result_(t)
|
||||
explicit wrapper(response_type* t = nullptr)
|
||||
: result_(t)
|
||||
{
|
||||
if (result_) {
|
||||
result_->value() = Result{};
|
||||
result_->value() = T{};
|
||||
impl_.on_value_available(result_->value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
operator()(
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { impl_.on_init(); }
|
||||
void on_done() { impl_.on_done(); }
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
|
||||
|
||||
if (result_->has_error())
|
||||
return;
|
||||
|
||||
if (set_if_resp3_error(nd))
|
||||
if (!std::exchange(called_once_, true) && set_if_resp3_error(nd))
|
||||
return;
|
||||
|
||||
BOOST_ASSERT(result_);
|
||||
impl_(result_->value(), nd, ec);
|
||||
impl_.on_node(result_->value(), nd, ec);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -413,26 +520,33 @@ public:
|
||||
private:
|
||||
response_type* result_;
|
||||
typename impl_map<T>::type impl_{};
|
||||
bool called_once_ = false;
|
||||
|
||||
bool set_if_resp3_error(resp3::basic_node<std::string_view> const& nd) noexcept
|
||||
template <class String>
|
||||
bool set_if_resp3_error(resp3::basic_node<String> const& nd) noexcept
|
||||
{
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::blob_error:
|
||||
case resp3::type::simple_error:
|
||||
*result_ = error{nd.data_type, {std::cbegin(nd.value), std::cend(nd.value)}};
|
||||
*result_ = error{
|
||||
nd.data_type,
|
||||
{std::cbegin(nd.value), std::cend(nd.value)}
|
||||
};
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit wrapper(response_type* o = nullptr) : result_(o) {}
|
||||
explicit wrapper(response_type* o = nullptr)
|
||||
: result_(o)
|
||||
{ }
|
||||
|
||||
void
|
||||
operator()(
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_init() { impl_.on_init(); }
|
||||
void on_done() { impl_.on_done(); }
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
|
||||
{
|
||||
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
|
||||
|
||||
@@ -442,18 +556,18 @@ public:
|
||||
if (set_if_resp3_error(nd))
|
||||
return;
|
||||
|
||||
if (nd.data_type == resp3::type::null)
|
||||
if (!std::exchange(called_once_, true) && nd.data_type == resp3::type::null)
|
||||
return;
|
||||
|
||||
if (!result_->value().has_value()) {
|
||||
result_->value() = T{};
|
||||
impl_.on_value_available(result_->value().value());
|
||||
result_->value() = T{};
|
||||
impl_.on_value_available(result_->value().value());
|
||||
}
|
||||
|
||||
impl_(result_->value().value(), nd, ec);
|
||||
impl_.on_node(result_->value().value(), nd, ec);
|
||||
}
|
||||
};
|
||||
|
||||
} // boost::redis::adapter::detail
|
||||
} // namespace boost::redis::adapter::detail
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_ADAPTERS_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_ADAPTERS_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,37 +7,20 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
|
||||
#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>
|
||||
#include <boost/redis/adapter/detail/result_traits.hpp>
|
||||
|
||||
#include <boost/mp11.hpp>
|
||||
#include <boost/system.hpp>
|
||||
|
||||
#include <tuple>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
|
||||
namespace boost::redis::adapter::detail
|
||||
{
|
||||
|
||||
class ignore_adapter {
|
||||
public:
|
||||
void
|
||||
operator()(std::size_t, 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;
|
||||
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);}
|
||||
};
|
||||
namespace boost::redis::adapter::detail {
|
||||
|
||||
template <class Response>
|
||||
class static_adapter {
|
||||
@@ -48,53 +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);
|
||||
}
|
||||
explicit static_adapter(Response& r) { assigner<size - 1>::assign(adapters_, r); }
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_supported_response_size() const noexcept
|
||||
{ return size;}
|
||||
|
||||
void
|
||||
operator()(
|
||||
std::size_t i,
|
||||
resp3::basic_node<std::string_view> 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);}, adapters_.at(i));
|
||||
visit(
|
||||
[&](auto& arg) {
|
||||
arg.on_init();
|
||||
},
|
||||
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
|
||||
{ return static_cast<std::size_t>(-1);}
|
||||
|
||||
void
|
||||
operator()(
|
||||
std::size_t,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
void on_done()
|
||||
{
|
||||
adapter_(nd, ec);
|
||||
using std::visit;
|
||||
visit(
|
||||
[&](auto& arg) {
|
||||
arg.on_done();
|
||||
},
|
||||
adapters_.at(i_));
|
||||
i_ += 1;
|
||||
}
|
||||
|
||||
template <class String>
|
||||
void on_node(resp3::basic_node<String> const& nd, system::error_code& 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,61 +78,35 @@ struct response_traits;
|
||||
template <>
|
||||
struct response_traits<ignore_t> {
|
||||
using response_type = ignore_t;
|
||||
using adapter_type = detail::ignore_adapter;
|
||||
using adapter_type = ignore;
|
||||
|
||||
static auto adapt(response_type&) noexcept
|
||||
{ return detail::ignore_adapter{}; }
|
||||
static auto adapt(response_type&) noexcept { return ignore{}; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct response_traits<result<ignore_t>> {
|
||||
using response_type = result<ignore_t>;
|
||||
using adapter_type = detail::ignore_adapter;
|
||||
using adapter_type = ignore;
|
||||
|
||||
static auto adapt(response_type&) noexcept
|
||||
{ return detail::ignore_adapter{}; }
|
||||
static auto adapt(response_type&) noexcept { return ignore{}; }
|
||||
};
|
||||
|
||||
template <class String, class Allocator>
|
||||
struct response_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
|
||||
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
|
||||
using adapter_type = vector_adapter<response_type>;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept
|
||||
{ return adapter_type{v}; }
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
};
|
||||
|
||||
template <class ...Ts>
|
||||
template <class... Ts>
|
||||
struct response_traits<response<Ts...>> {
|
||||
using response_type = response<Ts...>;
|
||||
using adapter_type = static_adapter<response_type>;
|
||||
|
||||
static auto adapt(response_type& r) noexcept
|
||||
{ return adapter_type{r}; }
|
||||
static auto adapt(response_type& r) noexcept { return adapter_type{r}; }
|
||||
};
|
||||
|
||||
template <class Adapter>
|
||||
class wrapper {
|
||||
public:
|
||||
explicit wrapper(Adapter adapter) : adapter_{adapter} {}
|
||||
} // namespace boost::redis::adapter::detail
|
||||
|
||||
void operator()(resp3::basic_node<std::string_view> 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};
|
||||
}
|
||||
|
||||
} // boost::redis::adapter::detail
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,21 +7,21 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
|
||||
#define BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
|
||||
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/adapter/detail/adapters.hpp>
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/adapter/ignore.hpp>
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/mp11.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::adapter::detail
|
||||
{
|
||||
namespace boost::redis::adapter::detail {
|
||||
|
||||
/* Traits class for response objects.
|
||||
*
|
||||
@@ -30,7 +30,7 @@ namespace boost::redis::adapter::detail
|
||||
*/
|
||||
template <class Result>
|
||||
struct result_traits {
|
||||
using adapter_type = adapter::detail::wrapper<typename std::decay<Result>::type>;
|
||||
using adapter_type = wrapper<typename std::decay<Result>::type>;
|
||||
static auto adapt(Result& r) noexcept { return adapter_type{&r}; }
|
||||
};
|
||||
|
||||
@@ -65,27 +65,29 @@ struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>>
|
||||
template <class T>
|
||||
using adapter_t = typename result_traits<std::decay_t<T>>::adapter_type;
|
||||
|
||||
template<class T>
|
||||
template <class T>
|
||||
auto internal_adapt(T& t) noexcept
|
||||
{ return result_traits<std::decay_t<T>>::adapt(t); }
|
||||
{
|
||||
return result_traits<std::decay_t<T>>::adapt(t);
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
struct assigner {
|
||||
template <class T1, class T2>
|
||||
static void assign(T1& dest, T2& from)
|
||||
{
|
||||
dest[N].template emplace<N>(internal_adapt(std::get<N>(from)));
|
||||
assigner<N - 1>::assign(dest, from);
|
||||
}
|
||||
template <class T1, class T2>
|
||||
static void assign(T1& dest, T2& from)
|
||||
{
|
||||
dest[N].template emplace<N>(internal_adapt(std::get<N>(from)));
|
||||
assigner<N - 1>::assign(dest, from);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct assigner<0> {
|
||||
template <class T1, class T2>
|
||||
static void assign(T1& dest, T2& from)
|
||||
{
|
||||
dest[0].template emplace<0>(internal_adapt(std::get<0>(from)));
|
||||
}
|
||||
template <class T1, class T2>
|
||||
static void assign(T1& dest, T2& from)
|
||||
{
|
||||
dest[0].template emplace<0>(internal_adapt(std::get<0>(from)));
|
||||
}
|
||||
};
|
||||
|
||||
template <class Tuple>
|
||||
@@ -94,16 +96,16 @@ class static_aggregate_adapter;
|
||||
template <class Tuple>
|
||||
class static_aggregate_adapter<result<Tuple>> {
|
||||
private:
|
||||
using adapters_array_type =
|
||||
std::array<
|
||||
mp11::mp_rename<
|
||||
mp11::mp_transform<
|
||||
adapter_t, Tuple>,
|
||||
std::variant>,
|
||||
std::tuple_size<Tuple>::value>;
|
||||
using adapters_array_type = std::array<
|
||||
mp11::mp_rename<mp11::mp_transform<adapter_t, Tuple>, std::variant>,
|
||||
std::tuple_size<Tuple>::value>;
|
||||
|
||||
// Tuple element we are currently on.
|
||||
std::size_t i_ = 0;
|
||||
|
||||
// Nested aggregate element counter.
|
||||
std::size_t aggregate_size_ = 0;
|
||||
|
||||
adapters_array_type adapters_;
|
||||
result<Tuple>* res_ = nullptr;
|
||||
|
||||
@@ -116,46 +118,74 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void count(resp3::basic_node<std::string_view> const& nd)
|
||||
template <class String>
|
||||
void count(resp3::basic_node<String> const& elem)
|
||||
{
|
||||
if (nd.depth == 1) {
|
||||
if (is_aggregate(nd.data_type))
|
||||
aggregate_size_ = element_multiplicity(nd.data_type) * nd.aggregate_size;
|
||||
else
|
||||
++i_;
|
||||
|
||||
return;
|
||||
if (elem.depth == 1 && is_aggregate(elem.data_type)) {
|
||||
aggregate_size_ = element_multiplicity(elem.data_type) * elem.aggregate_size;
|
||||
}
|
||||
|
||||
if (--aggregate_size_ == 0)
|
||||
++i_;
|
||||
if (aggregate_size_ == 0) {
|
||||
i_ += 1;
|
||||
} else {
|
||||
aggregate_size_ -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code& ec)
|
||||
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 on_node(resp3::basic_node<String> const& elem, system::error_code& ec)
|
||||
{
|
||||
using std::visit;
|
||||
|
||||
if (nd.depth == 0) {
|
||||
auto const real_aggr_size = nd.aggregate_size * element_multiplicity(nd.data_type);
|
||||
if (elem.depth == 0) {
|
||||
auto const multiplicity = element_multiplicity(elem.data_type);
|
||||
auto const real_aggr_size = elem.aggregate_size * multiplicity;
|
||||
if (real_aggr_size != std::tuple_size<Tuple>::value)
|
||||
ec = redis::error::incompatible_size;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
visit([&](auto& arg){arg(nd, ec);}, adapters_[i_]);
|
||||
count(nd);
|
||||
visit(
|
||||
[&](auto& arg) {
|
||||
arg.on_node(elem, ec);
|
||||
},
|
||||
adapters_.at(i_));
|
||||
count(elem);
|
||||
}
|
||||
};
|
||||
|
||||
template <class... Ts>
|
||||
struct result_traits<result<std::tuple<Ts...>>>
|
||||
{
|
||||
struct result_traits<result<std::tuple<Ts...>>> {
|
||||
using response_type = result<std::tuple<Ts...>>;
|
||||
using adapter_type = static_aggregate_adapter<response_type>;
|
||||
static auto adapt(response_type& r) noexcept { return adapter_type{&r}; }
|
||||
};
|
||||
|
||||
} // boost::redis::adapter::detail
|
||||
} // namespace boost::redis::adapter::detail
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,31 +7,32 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_IGNORE_HPP
|
||||
#define BOOST_REDIS_ADAPTER_IGNORE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis::adapter
|
||||
{
|
||||
namespace boost::redis::adapter {
|
||||
|
||||
/** @brief An adapter that ignores responses
|
||||
* @ingroup high-level-api
|
||||
/** @brief An adapter that ignores responses.
|
||||
*
|
||||
* 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;
|
||||
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
|
||||
case resp3::type::null: ec = redis::error::resp3_null; break;
|
||||
default:;
|
||||
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
|
||||
case resp3::type::null: ec = redis::error::resp3_null; break;
|
||||
default: ;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // boost::redis::adapter
|
||||
} // namespace boost::redis::adapter
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_IGNORE_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_IGNORE_HPP
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -8,17 +8,16 @@
|
||||
#ifndef BOOST_REDIS_ADAPTER_RESULT_HPP
|
||||
#define BOOST_REDIS_ADAPTER_RESULT_HPP
|
||||
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/system/result.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis::adapter
|
||||
{
|
||||
namespace boost::redis::adapter {
|
||||
|
||||
/** @brief Stores any resp3 error
|
||||
* @ingroup high-level-api
|
||||
*/
|
||||
/// Stores any resp3 error.
|
||||
struct error {
|
||||
/// RESP3 error data type.
|
||||
resp3::type data_type = resp3::type::invalid;
|
||||
@@ -44,38 +43,30 @@ inline bool operator==(error const& a, error const& b)
|
||||
* @param a Left hand side error object.
|
||||
* @param b Right hand side error object.
|
||||
*/
|
||||
inline bool operator!=(error const& a, error const& b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
inline bool operator!=(error const& a, error const& b) { return !(a == b); }
|
||||
|
||||
/** @brief Stores response to individual Redis commands
|
||||
* @ingroup high-level-api
|
||||
*/
|
||||
/// Stores response to individual Redis commands.
|
||||
template <class Value>
|
||||
using result = system::result<Value, error>;
|
||||
|
||||
BOOST_NORETURN inline void
|
||||
throw_exception_from_error(error const & e, boost::source_location const &)
|
||||
/**
|
||||
* @brief Allows using @ref error with `boost::system::result`.
|
||||
* @param e The error to throw.
|
||||
* @relates 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.");
|
||||
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);
|
||||
}
|
||||
|
||||
} // boost::redis::adapter
|
||||
} // namespace boost::redis::adapter
|
||||
|
||||
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP
|
||||
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,16 +7,14 @@
|
||||
#ifndef BOOST_REDIS_CONFIG_HPP
|
||||
#define BOOST_REDIS_CONFIG_HPP
|
||||
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis
|
||||
{
|
||||
namespace boost::redis {
|
||||
|
||||
/** @brief Address of a Redis server
|
||||
* @ingroup high-level-api
|
||||
*/
|
||||
/// Address of a Redis server.
|
||||
struct address {
|
||||
/// Redis host.
|
||||
std::string host = "127.0.0.1";
|
||||
@@ -24,62 +22,89 @@ struct address {
|
||||
std::string port = "6379";
|
||||
};
|
||||
|
||||
/** @brief Configure parameters used by the connection classes
|
||||
* @ingroup high-level-api
|
||||
*/
|
||||
/// Configure parameters used by the connection classes.
|
||||
struct config {
|
||||
/// Uses SSL instead of a plain connection.
|
||||
bool use_ssl = false;
|
||||
|
||||
/// Address of the Redis server.
|
||||
/// For TCP connections, hostname and port of the Redis server.
|
||||
address addr = address{"127.0.0.1", "6379"};
|
||||
|
||||
/** @brief Username passed to the
|
||||
* [HELLO](https://redis.io/commands/hello/) command. If left
|
||||
* empty `HELLO` will be sent without authentication parameters.
|
||||
/**
|
||||
* @brief The UNIX domain socket path where the server is listening.
|
||||
*
|
||||
* 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`.
|
||||
*/
|
||||
std::string username;
|
||||
std::string unix_socket;
|
||||
|
||||
/** @brief Username passed to the `HELLO` command.
|
||||
* If left empty `HELLO` will be sent without authentication parameters.
|
||||
*/
|
||||
std::string username = "default";
|
||||
|
||||
/** @brief Password passed to the
|
||||
* [HELLO](https://redis.io/commands/hello/) command. If left
|
||||
* `HELLO` command. If left
|
||||
* empty `HELLO` will be sent without authentication parameters.
|
||||
*/
|
||||
std::string password;
|
||||
|
||||
/// Client name parameter of the [HELLO](https://redis.io/commands/hello/) command.
|
||||
/// Client name parameter of the `HELLO` command.
|
||||
std::string clientname = "Boost.Redis";
|
||||
|
||||
/// Database that will be passed to the [SELECT](https://redis.io/commands/hello/) command.
|
||||
/// Database that will be passed to the `SELECT` command.
|
||||
std::optional<int> database_index = 0;
|
||||
|
||||
/// Message used by the health-checker in `boost::redis::connection::async_run`.
|
||||
/// Message used by the health-checker in @ref boost::redis::basic_connection::async_run.
|
||||
std::string health_check_id = "Boost.Redis";
|
||||
|
||||
/// Logger prefix, see `boost::redis::logger`.
|
||||
/**
|
||||
* @brief (Deprecated) Sets the logger prefix, a string printed before log messages.
|
||||
*
|
||||
* Setting a prefix in this struct is deprecated. If you need to change how log messages
|
||||
* look like, please construct a logger object passing a formatting function, and use that
|
||||
* logger in connection's constructor. This member will be removed in subsequent releases.
|
||||
*/
|
||||
std::string log_prefix = "(Boost.Redis) ";
|
||||
|
||||
/// Time the resolve operation is allowed to last.
|
||||
/// Time span that the resolve operation is allowed to elapse.
|
||||
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time the connect operation is allowed to last.
|
||||
/// Time span that the connect operation is allowed to elapse.
|
||||
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time the SSL handshake operation is allowed to last.
|
||||
/// Time span that the SSL handshake operation is allowed to elapse.
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
|
||||
|
||||
/** Health checks interval.
|
||||
*
|
||||
* To disable health-checks pass zero as duration.
|
||||
/** @brief Time span between successive health checks.
|
||||
* Set to zero to disable health-checks pass zero as duration.
|
||||
*/
|
||||
std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2};
|
||||
|
||||
/** @brief Time waited before trying a reconnection.
|
||||
*
|
||||
* To disable reconnection pass zero as duration.
|
||||
/** @brief Time span to wait between successive connection retries.
|
||||
* Set to zero to disable reconnection.
|
||||
*/
|
||||
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
|
||||
|
||||
/** @brief Maximum size of the socket read-buffer in bytes.
|
||||
*
|
||||
* Sets a limit on how much data is allowed to be read into the
|
||||
* read buffer. It can be used to prevent DDOS.
|
||||
*/
|
||||
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
|
||||
|
||||
/** @brief read_buffer_append_size
|
||||
*
|
||||
* The size by which the read buffer grows when more space is
|
||||
* needed. This can help avoiding some memory allocations. Once the
|
||||
* maximum size is reached no more memory allocations are made
|
||||
* since the buffer is reused.
|
||||
*/
|
||||
std::size_t read_buffer_append_size = 4096;
|
||||
};
|
||||
|
||||
} // boost::redis
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_CONFIG_HPP
|
||||
#endif // BOOST_REDIS_CONFIG_HPP
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,920 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_CONNECTION_BASE_HPP
|
||||
#define BOOST_REDIS_CONNECTION_BASE_HPP
|
||||
|
||||
#include <boost/redis/adapter/adapt.hpp>
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/detail/read.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/runner.hpp>
|
||||
|
||||
#include <boost/system.hpp>
|
||||
#include <boost/asio/basic_stream_socket.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
template <class Conn>
|
||||
struct wait_receive_op {
|
||||
Conn* conn_;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void
|
||||
operator()(Self& self , system::error_code ec = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
conn_->read_op_timer_.cancel();
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->read_op_timer_.async_wait(std::move(self));
|
||||
if (!conn_->is_open() || is_cancelled(self)) {
|
||||
self.complete(!!ec ? ec : asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
self.complete({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn, class Adapter>
|
||||
class read_next_op {
|
||||
public:
|
||||
using req_info_type = typename Conn::req_info;
|
||||
using req_info_ptr = typename std::shared_ptr<req_info_type>;
|
||||
|
||||
private:
|
||||
Conn* conn_;
|
||||
req_info_ptr info_;
|
||||
Adapter adapter_;
|
||||
std::size_t cmds_ = 0;
|
||||
std::size_t read_size_ = 0;
|
||||
std::size_t index_ = 0;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
public:
|
||||
read_next_op(Conn& conn, Adapter adapter, req_info_ptr info)
|
||||
: conn_{&conn}
|
||||
, info_{info}
|
||||
, adapter_{adapter}
|
||||
, cmds_{info->get_number_of_commands()}
|
||||
{}
|
||||
|
||||
auto make_adapter() noexcept
|
||||
{
|
||||
return [i = index_, adpt = adapter_] (resp3::basic_node<std::string_view> const& nd, system::error_code& ec) mutable { adpt(i, nd, ec); };
|
||||
}
|
||||
|
||||
template <class Self>
|
||||
void
|
||||
operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
// Loop reading the responses to this request.
|
||||
while (cmds_ != 0) {
|
||||
if (info_->stop_requested()) {
|
||||
self.complete(asio::error::operation_aborted, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
//-----------------------------------
|
||||
// If we detect a push in the middle of a request we have
|
||||
// to hand it to the push consumer. To do that we need
|
||||
// some data in the read bufer.
|
||||
if (conn_->read_buffer_.empty()) {
|
||||
|
||||
if (conn_->use_ssl()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::async_read_until(conn_->next_layer(), conn_->dbuf_, resp3::parser::sep, std::move(self));
|
||||
} else {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::async_read_until(conn_->next_layer().next_layer(), conn_->dbuf_, resp3::parser::sep, std::move(self));
|
||||
}
|
||||
|
||||
BOOST_REDIS_CHECK_OP1(conn_->cancel(operation::run););
|
||||
if (info_->stop_requested()) {
|
||||
self.complete(asio::error::operation_aborted, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the next request is a push we have to handle it to
|
||||
// the receive_op wait for it to be done and continue.
|
||||
if (resp3::to_type(conn_->read_buffer_.front()) == resp3::type::push) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->async_wait_receive(std::move(self));
|
||||
BOOST_REDIS_CHECK_OP1(conn_->cancel(operation::run););
|
||||
continue;
|
||||
}
|
||||
//-----------------------------------
|
||||
|
||||
if (conn_->use_ssl()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
redis::detail::async_read(conn_->next_layer(), conn_->dbuf_, make_adapter(), std::move(self));
|
||||
} else {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
redis::detail::async_read(conn_->next_layer().next_layer(), conn_->dbuf_, make_adapter(), std::move(self));
|
||||
}
|
||||
|
||||
++index_;
|
||||
|
||||
if (ec || redis::detail::is_cancelled(self)) {
|
||||
conn_->cancel(operation::run);
|
||||
self.complete(!!ec ? ec : asio::error::operation_aborted, {});
|
||||
return;
|
||||
}
|
||||
|
||||
conn_->dbuf_.consume(n);
|
||||
read_size_ += n;
|
||||
|
||||
BOOST_ASSERT(cmds_ != 0);
|
||||
--cmds_;
|
||||
}
|
||||
|
||||
self.complete({}, read_size_);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn, class Adapter>
|
||||
struct receive_op {
|
||||
Conn* conn_;
|
||||
Adapter adapter;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void
|
||||
operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
if (!conn_->is_next_push()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->read_op_timer_.async_wait(std::move(self));
|
||||
if (!conn_->is_open() || is_cancelled(self)) {
|
||||
self.complete(!!ec ? ec : asio::error::operation_aborted, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (conn_->use_ssl()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
redis::detail::async_read(conn_->next_layer(), conn_->dbuf_, adapter, std::move(self));
|
||||
} else {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
redis::detail::async_read(conn_->next_layer().next_layer(), conn_->dbuf_, adapter, std::move(self));
|
||||
}
|
||||
|
||||
if (ec || is_cancelled(self)) {
|
||||
conn_->cancel(operation::run);
|
||||
conn_->cancel(operation::receive);
|
||||
self.complete(!!ec ? ec : asio::error::operation_aborted, {});
|
||||
return;
|
||||
}
|
||||
|
||||
conn_->dbuf_.consume(n);
|
||||
|
||||
if (!conn_->is_next_push()) {
|
||||
conn_->read_op_timer_.cancel();
|
||||
}
|
||||
|
||||
self.complete({}, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn, class Adapter>
|
||||
struct exec_op {
|
||||
using req_info_type = typename Conn::req_info;
|
||||
|
||||
Conn* conn = nullptr;
|
||||
request const* req = nullptr;
|
||||
Adapter adapter{};
|
||||
std::shared_ptr<req_info_type> info = nullptr;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void
|
||||
operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
// Check whether the user wants to wait for the connection to
|
||||
// be stablished.
|
||||
if (req->get_config().cancel_if_not_connected && !conn->is_open()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::post(std::move(self));
|
||||
return self.complete(error::not_connected, 0);
|
||||
}
|
||||
|
||||
info = std::allocate_shared<req_info_type>(asio::get_associated_allocator(self), *req, conn->get_executor());
|
||||
|
||||
conn->add_request_info(info);
|
||||
EXEC_OP_WAIT:
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
info->async_wait(std::move(self));
|
||||
BOOST_ASSERT(ec == asio::error::operation_aborted);
|
||||
|
||||
if (info->stop_requested()) {
|
||||
// Don't have to call remove_request as it has already
|
||||
// been by cancel(exec).
|
||||
return self.complete(ec, 0);
|
||||
}
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
if (info->is_written()) {
|
||||
using c_t = asio::cancellation_type;
|
||||
auto const c = self.get_cancellation_state().cancelled();
|
||||
if ((c & c_t::terminal) != c_t::none) {
|
||||
// Cancellation requires closing the connection
|
||||
// otherwise it stays in inconsistent state.
|
||||
conn->cancel(operation::run);
|
||||
return self.complete(ec, 0);
|
||||
} else {
|
||||
// Can't implement other cancelation types, ignoring.
|
||||
self.get_cancellation_state().clear();
|
||||
goto EXEC_OP_WAIT;
|
||||
}
|
||||
} else {
|
||||
// Cancelation can be honored.
|
||||
conn->remove_request(info);
|
||||
self.complete(ec, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASSERT(conn->is_open());
|
||||
|
||||
if (req->size() == 0) {
|
||||
// Don't have to call remove_request as it has already
|
||||
// been removed.
|
||||
return self.complete({}, 0);
|
||||
}
|
||||
|
||||
BOOST_ASSERT(!conn->reqs_.empty());
|
||||
BOOST_ASSERT(conn->reqs_.front() != nullptr);
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn->async_read_next(adapter, std::move(self));
|
||||
BOOST_REDIS_CHECK_OP1(;);
|
||||
|
||||
if (info->stop_requested()) {
|
||||
// Don't have to call remove_request as it has already
|
||||
// been by cancel(exec).
|
||||
return self.complete(ec, 0);
|
||||
}
|
||||
|
||||
BOOST_ASSERT(!conn->reqs_.empty());
|
||||
conn->reqs_.pop_front();
|
||||
|
||||
if (conn->is_waiting_response()) {
|
||||
BOOST_ASSERT(!conn->reqs_.empty());
|
||||
conn->reqs_.front()->proceed();
|
||||
} else {
|
||||
conn->read_timer_.cancel_one();
|
||||
}
|
||||
|
||||
self.complete({}, n);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn, class Logger>
|
||||
struct run_op {
|
||||
Conn* conn = nullptr;
|
||||
Logger logger_;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, std::array<std::size_t, 2> order = {}
|
||||
, system::error_code ec0 = {}
|
||||
, system::error_code ec1 = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
conn->write_buffer_.clear();
|
||||
conn->read_buffer_.clear();
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token) { return conn->reader(token);},
|
||||
[this](auto token) { return conn->writer(logger_, token);}
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_one(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (order[0]) {
|
||||
case 0: self.complete(ec0); break;
|
||||
case 1: self.complete(ec1); break;
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn, class Logger>
|
||||
struct writer_op {
|
||||
Conn* conn_;
|
||||
Logger logger_;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
ignore_unused(n);
|
||||
|
||||
BOOST_ASIO_CORO_REENTER (coro) for (;;)
|
||||
{
|
||||
while (conn_->coalesce_requests()) {
|
||||
if (conn_->use_ssl())
|
||||
BOOST_ASIO_CORO_YIELD asio::async_write(conn_->next_layer(), asio::buffer(conn_->write_buffer_), std::move(self));
|
||||
else
|
||||
BOOST_ASIO_CORO_YIELD asio::async_write(conn_->next_layer().next_layer(), asio::buffer(conn_->write_buffer_), std::move(self));
|
||||
|
||||
logger_.on_write(ec, conn_->write_buffer_);
|
||||
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run););
|
||||
|
||||
conn_->on_write();
|
||||
|
||||
// A socket.close() may have been called while a
|
||||
// successful write might had already been queued, so we
|
||||
// have to check here before proceeding.
|
||||
if (!conn_->is_open()) {
|
||||
self.complete({});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->writer_timer_.async_wait(std::move(self));
|
||||
if (!conn_->is_open() || is_cancelled(self)) {
|
||||
// Notice this is not an error of the op, stoping was
|
||||
// requested from the outside, so we complete with
|
||||
// success.
|
||||
self.complete({});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Conn>
|
||||
struct reader_op {
|
||||
Conn* conn;
|
||||
asio::coroutine coro{};
|
||||
|
||||
bool as_push() const
|
||||
{
|
||||
return
|
||||
(resp3::to_type(conn->read_buffer_.front()) == resp3::type::push)
|
||||
|| conn->reqs_.empty()
|
||||
|| (!conn->reqs_.empty() && conn->reqs_.front()->get_number_of_commands() == 0)
|
||||
|| !conn->is_waiting_response(); // Added to deal with MONITOR.
|
||||
}
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
ignore_unused(n);
|
||||
|
||||
BOOST_ASIO_CORO_REENTER (coro) for (;;)
|
||||
{
|
||||
if (conn->use_ssl())
|
||||
BOOST_ASIO_CORO_YIELD asio::async_read_until(conn->next_layer(), conn->dbuf_, "\r\n", std::move(self));
|
||||
else
|
||||
BOOST_ASIO_CORO_YIELD asio::async_read_until(conn->next_layer().next_layer(), conn->dbuf_, "\r\n", std::move(self));
|
||||
|
||||
if (ec == asio::error::eof) {
|
||||
conn->cancel(operation::run);
|
||||
return self.complete({}); // EOFINAE: EOF is not an error.
|
||||
}
|
||||
|
||||
BOOST_REDIS_CHECK_OP0(conn->cancel(operation::run););
|
||||
|
||||
// We handle unsolicited events in the following way
|
||||
//
|
||||
// 1. Its resp3 type is a push.
|
||||
//
|
||||
// 2. A non-push type is received with an empty requests
|
||||
// queue. I have noticed this is possible (e.g. -MISCONF).
|
||||
// I expect them to have type push so we can distinguish
|
||||
// them from responses to commands, but it is a
|
||||
// simple-error. If we are lucky enough to receive them
|
||||
// when the command queue is empty we can treat them as
|
||||
// server pushes, otherwise it is impossible to handle
|
||||
// them properly
|
||||
//
|
||||
// 3. The request does not expect any response but we got
|
||||
// one. This may happen if for example, subscribe with
|
||||
// wrong syntax.
|
||||
//
|
||||
// Useful links:
|
||||
//
|
||||
// - https://github.com/redis/redis/issues/11784
|
||||
// - https://github.com/redis/redis/issues/6426
|
||||
//
|
||||
BOOST_ASSERT(!conn->read_buffer_.empty());
|
||||
if (as_push()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn->async_wait_receive(std::move(self));
|
||||
} else {
|
||||
BOOST_ASSERT_MSG(conn->is_waiting_response(), "Not waiting for a response (using MONITOR command perhaps?)");
|
||||
BOOST_ASSERT(!conn->reqs_.empty());
|
||||
BOOST_ASSERT(conn->reqs_.front()->get_number_of_commands() != 0);
|
||||
conn->reqs_.front()->proceed();
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn->read_timer_.async_wait(std::move(self));
|
||||
ec = {};
|
||||
}
|
||||
|
||||
if (!conn->is_open() || ec || is_cancelled(self)) {
|
||||
conn->cancel(operation::run);
|
||||
self.complete(asio::error::basic_errors::operation_aborted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Base class for high level Redis asynchronous connections.
|
||||
* @ingroup high-level-api
|
||||
*
|
||||
* @tparam Executor The executor type.
|
||||
*
|
||||
*/
|
||||
template <class Executor>
|
||||
class connection_base {
|
||||
public:
|
||||
/// Executor type
|
||||
using executor_type = Executor;
|
||||
|
||||
/// Type of the next layer
|
||||
using next_layer_type = asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp, Executor>>;
|
||||
|
||||
using this_type = connection_base<Executor>;
|
||||
|
||||
/// Constructs from an executor.
|
||||
connection_base(
|
||||
executor_type ex,
|
||||
asio::ssl::context::method method,
|
||||
std::size_t max_read_size)
|
||||
: ctx_{method}
|
||||
, stream_{std::make_unique<next_layer_type>(ex, ctx_)}
|
||||
, writer_timer_{ex}
|
||||
, read_timer_{ex}
|
||||
, read_op_timer_{ex}
|
||||
, runner_{ex, {}}
|
||||
, dbuf_{read_buffer_, max_read_size}
|
||||
{
|
||||
writer_timer_.expires_at(std::chrono::steady_clock::time_point::max());
|
||||
read_timer_.expires_at(std::chrono::steady_clock::time_point::max());
|
||||
read_op_timer_.expires_at(std::chrono::steady_clock::time_point::max());
|
||||
}
|
||||
|
||||
/// Returns the ssl context.
|
||||
auto const& get_ssl_context() const noexcept
|
||||
{ return ctx_;}
|
||||
|
||||
/// Returns the ssl context.
|
||||
auto& get_ssl_context() noexcept
|
||||
{ return ctx_;}
|
||||
|
||||
/// Resets the underlying stream.
|
||||
void reset_stream()
|
||||
{
|
||||
stream_ = std::make_unique<next_layer_type>(writer_timer_.get_executor(), ctx_);
|
||||
}
|
||||
|
||||
/// Returns a reference to the next layer.
|
||||
auto& next_layer() noexcept { return *stream_; }
|
||||
|
||||
/// Returns a const reference to the next layer.
|
||||
auto const& next_layer() const noexcept { return *stream_; }
|
||||
|
||||
/// Returns the associated executor.
|
||||
auto get_executor() {return writer_timer_.get_executor();}
|
||||
|
||||
/// Cancels specific operations.
|
||||
virtual void cancel(operation op)
|
||||
{
|
||||
runner_.cancel(op);
|
||||
if (op == operation::all) {
|
||||
cancel_impl(operation::run);
|
||||
cancel_impl(operation::receive);
|
||||
cancel_impl(operation::exec);
|
||||
return;
|
||||
}
|
||||
|
||||
cancel_impl(op);
|
||||
}
|
||||
|
||||
template <class Response, class CompletionToken>
|
||||
auto async_exec(request const& req, Response& resp, CompletionToken token)
|
||||
{
|
||||
using namespace boost::redis::adapter;
|
||||
auto f = boost_redis_adapt(resp);
|
||||
BOOST_ASSERT_MSG(req.size() <= f.get_supported_response_size(), "Request and response have incompatible sizes.");
|
||||
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code, std::size_t)
|
||||
>(redis::detail::exec_op<this_type, decltype(f)>{this, &req, f}, token, writer_timer_);
|
||||
}
|
||||
|
||||
template <class Response, class CompletionToken>
|
||||
auto async_receive(Response& response, CompletionToken token)
|
||||
{
|
||||
using namespace boost::redis::adapter;
|
||||
auto g = boost_redis_adapt(response);
|
||||
auto f = adapter::detail::make_adapter_wrapper(g);
|
||||
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code, std::size_t)
|
||||
>(redis::detail::receive_op<this_type, decltype(f)>{this, f}, token, read_op_timer_);
|
||||
}
|
||||
|
||||
template <class Logger, class CompletionToken>
|
||||
auto async_run(config const& cfg, Logger l, CompletionToken token)
|
||||
{
|
||||
runner_.set_config(cfg);
|
||||
l.set_prefix(runner_.get_config().log_prefix);
|
||||
return runner_.async_run(*this, l, std::move(token));
|
||||
}
|
||||
|
||||
private:
|
||||
using clock_type = std::chrono::steady_clock;
|
||||
using clock_traits_type = asio::wait_traits<clock_type>;
|
||||
using timer_type = asio::basic_waitable_timer<clock_type, clock_traits_type, executor_type>;
|
||||
using runner_type = redis::detail::runner<executor_type>;
|
||||
|
||||
auto use_ssl() const noexcept
|
||||
{ return runner_.get_config().use_ssl;}
|
||||
|
||||
auto cancel_on_conn_lost() -> std::size_t
|
||||
{
|
||||
// Must return false if the request should be removed.
|
||||
auto cond = [](auto const& ptr)
|
||||
{
|
||||
BOOST_ASSERT(ptr != nullptr);
|
||||
|
||||
if (ptr->is_written()) {
|
||||
return !ptr->get_request().get_config().cancel_if_unresponded;
|
||||
} else {
|
||||
return !ptr->get_request().get_config().cancel_on_connection_lost;
|
||||
}
|
||||
};
|
||||
|
||||
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->stop();
|
||||
});
|
||||
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
|
||||
return ptr->reset_status();
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto cancel_unwritten_requests() -> std::size_t
|
||||
{
|
||||
auto f = [](auto const& ptr)
|
||||
{
|
||||
BOOST_ASSERT(ptr != nullptr);
|
||||
return ptr->is_written();
|
||||
};
|
||||
|
||||
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), f);
|
||||
|
||||
auto const ret = std::distance(point, std::end(reqs_));
|
||||
|
||||
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
|
||||
ptr->stop();
|
||||
});
|
||||
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
return ret;
|
||||
}
|
||||
|
||||
void cancel_impl(operation op)
|
||||
{
|
||||
switch (op) {
|
||||
case operation::exec:
|
||||
{
|
||||
cancel_unwritten_requests();
|
||||
} break;
|
||||
case operation::run:
|
||||
{
|
||||
close();
|
||||
read_timer_.cancel();
|
||||
writer_timer_.cancel();
|
||||
cancel_on_conn_lost();
|
||||
} break;
|
||||
case operation::receive:
|
||||
{
|
||||
read_op_timer_.cancel();
|
||||
} break;
|
||||
default: /* ignore */;
|
||||
}
|
||||
}
|
||||
|
||||
void on_write()
|
||||
{
|
||||
// We have to clear the payload right after writing it to use it
|
||||
// as a flag that informs there is no ongoing write.
|
||||
write_buffer_.clear();
|
||||
|
||||
// Notice this must come before the for-each below.
|
||||
cancel_push_requests();
|
||||
|
||||
// There is small optimization possible here: traverse only the
|
||||
// partition of unwritten requests instead of them all.
|
||||
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
|
||||
BOOST_ASSERT_MSG(ptr != nullptr, "Expects non-null pointer.");
|
||||
if (ptr->is_staged())
|
||||
ptr->mark_written();
|
||||
});
|
||||
}
|
||||
|
||||
struct req_info {
|
||||
public:
|
||||
enum class action
|
||||
{
|
||||
stop,
|
||||
proceed,
|
||||
none,
|
||||
};
|
||||
|
||||
explicit req_info(request const& req, executor_type ex)
|
||||
: timer_{ex}
|
||||
, action_{action::none}
|
||||
, req_{&req}
|
||||
, cmds_{std::size(req)}
|
||||
, status_{status::none}
|
||||
{
|
||||
timer_.expires_at(std::chrono::steady_clock::time_point::max());
|
||||
}
|
||||
|
||||
auto proceed()
|
||||
{
|
||||
timer_.cancel();
|
||||
action_ = action::proceed;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
timer_.cancel();
|
||||
action_ = action::stop;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_waiting_write() const noexcept
|
||||
{ return !is_written() && !is_staged(); }
|
||||
|
||||
[[nodiscard]] auto is_written() const noexcept
|
||||
{ return status_ == status::written; }
|
||||
|
||||
[[nodiscard]] auto is_staged() const noexcept
|
||||
{ return status_ == status::staged; }
|
||||
|
||||
void mark_written() noexcept
|
||||
{ status_ = status::written; }
|
||||
|
||||
void mark_staged() noexcept
|
||||
{ status_ = status::staged; }
|
||||
|
||||
void reset_status() noexcept
|
||||
{ status_ = status::none; }
|
||||
|
||||
[[nodiscard]] auto get_number_of_commands() const noexcept
|
||||
{ return cmds_; }
|
||||
|
||||
[[nodiscard]] auto get_request() const noexcept -> auto const&
|
||||
{ return *req_; }
|
||||
|
||||
[[nodiscard]] auto stop_requested() const noexcept
|
||||
{ return action_ == action::stop;}
|
||||
|
||||
template <class CompletionToken>
|
||||
auto async_wait(CompletionToken token)
|
||||
{
|
||||
return timer_.async_wait(std::move(token));
|
||||
}
|
||||
|
||||
private:
|
||||
enum class status
|
||||
{ none
|
||||
, staged
|
||||
, written
|
||||
};
|
||||
|
||||
timer_type timer_;
|
||||
action action_;
|
||||
request const* req_;
|
||||
std::size_t cmds_;
|
||||
status status_;
|
||||
};
|
||||
|
||||
void remove_request(std::shared_ptr<req_info> const& info)
|
||||
{
|
||||
reqs_.erase(std::remove(std::begin(reqs_), std::end(reqs_), info));
|
||||
}
|
||||
|
||||
using reqs_type = std::deque<std::shared_ptr<req_info>>;
|
||||
|
||||
template <class> friend struct redis::detail::reader_op;
|
||||
template <class, class> friend struct redis::detail::writer_op;
|
||||
template <class, class> friend struct redis::detail::run_op;
|
||||
template <class, class> friend struct redis::detail::exec_op;
|
||||
template <class, class> friend class redis::detail::read_next_op;
|
||||
template <class, class> friend struct redis::detail::receive_op;
|
||||
template <class> friend struct redis::detail::wait_receive_op;
|
||||
template <class, class, class> friend struct redis::detail::run_all_op;
|
||||
|
||||
template <class CompletionToken>
|
||||
auto async_wait_receive(CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(redis::detail::wait_receive_op<this_type>{this}, token, read_op_timer_);
|
||||
}
|
||||
|
||||
void cancel_push_requests()
|
||||
{
|
||||
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
|
||||
return !(ptr->is_staged() && ptr->get_request().size() == 0);
|
||||
});
|
||||
|
||||
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
|
||||
ptr->proceed();
|
||||
});
|
||||
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_writing() const noexcept
|
||||
{
|
||||
return !write_buffer_.empty();
|
||||
}
|
||||
|
||||
void add_request_info(std::shared_ptr<req_info> const& info)
|
||||
{
|
||||
reqs_.push_back(info);
|
||||
|
||||
if (info->get_request().has_hello_priority()) {
|
||||
auto rend = std::partition_point(std::rbegin(reqs_), std::rend(reqs_), [](auto const& e) {
|
||||
return e->is_waiting_write();
|
||||
});
|
||||
|
||||
std::rotate(std::rbegin(reqs_), std::rbegin(reqs_) + 1, rend);
|
||||
}
|
||||
|
||||
if (is_open() && !is_writing())
|
||||
writer_timer_.cancel();
|
||||
}
|
||||
|
||||
template <class CompletionToken>
|
||||
auto reader(CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(redis::detail::reader_op<this_type>{this}, token, writer_timer_);
|
||||
}
|
||||
|
||||
template <class CompletionToken, class Logger>
|
||||
auto writer(Logger l, CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(redis::detail::writer_op<this_type, Logger>{this, l}, token, writer_timer_);
|
||||
}
|
||||
|
||||
template <class Adapter, class CompletionToken>
|
||||
auto async_read_next(Adapter adapter, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code, std::size_t)
|
||||
>(redis::detail::read_next_op<this_type, Adapter>{*this, adapter, reqs_.front()}, token, writer_timer_);
|
||||
}
|
||||
|
||||
template <class Logger, class CompletionToken>
|
||||
auto async_run_lean(config const& cfg, Logger l, CompletionToken token)
|
||||
{
|
||||
runner_.set_config(cfg);
|
||||
l.set_prefix(runner_.get_config().log_prefix);
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(redis::detail::run_op<this_type, Logger>{this, l}, token, writer_timer_);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool coalesce_requests()
|
||||
{
|
||||
// Coalesces the requests and marks them staged. After a
|
||||
// successful write staged requests will be marked as written.
|
||||
auto const point = std::partition_point(std::cbegin(reqs_), std::cend(reqs_), [](auto const& ri) {
|
||||
return !ri->is_waiting_write();
|
||||
});
|
||||
|
||||
std::for_each(point, std::cend(reqs_), [this](auto const& ri) {
|
||||
// Stage the request.
|
||||
write_buffer_ += ri->get_request().payload();
|
||||
ri->mark_staged();
|
||||
});
|
||||
|
||||
return point != std::cend(reqs_);
|
||||
}
|
||||
|
||||
bool is_waiting_response() const noexcept
|
||||
{
|
||||
return !std::empty(reqs_) && reqs_.front()->is_written();
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
if (stream_->next_layer().is_open())
|
||||
stream_->next_layer().close();
|
||||
}
|
||||
|
||||
bool is_next_push() const noexcept
|
||||
{
|
||||
return !read_buffer_.empty() && (resp3::to_type(read_buffer_.front()) == resp3::type::push);
|
||||
}
|
||||
|
||||
auto is_open() const noexcept { return stream_->next_layer().is_open(); }
|
||||
auto& lowest_layer() noexcept { return stream_->lowest_layer(); }
|
||||
|
||||
asio::ssl::context ctx_;
|
||||
std::unique_ptr<next_layer_type> stream_;
|
||||
|
||||
// Notice we use a timer to simulate a condition-variable. It is
|
||||
// also more suitable than a channel and the notify operation does
|
||||
// not suspend.
|
||||
timer_type writer_timer_;
|
||||
timer_type read_timer_;
|
||||
timer_type read_op_timer_;
|
||||
runner_type runner_;
|
||||
|
||||
using dyn_buffer_type = asio::dynamic_string_buffer<char, std::char_traits<char>, std::allocator<char>>;
|
||||
|
||||
std::string read_buffer_;
|
||||
dyn_buffer_type dbuf_;
|
||||
std::string write_buffer_;
|
||||
reqs_type reqs_;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTION_BASE_HPP
|
||||
54
include/boost/redis/detail/connection_logger.hpp
Normal file
54
include/boost/redis/detail/connection_logger.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/* 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
|
||||
@@ -1,133 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_CONNECTOR_HPP
|
||||
#define BOOST_REDIS_CONNECTOR_HPP
|
||||
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
namespace boost::redis::detail
|
||||
{
|
||||
|
||||
template <class Connector, class Stream>
|
||||
struct connect_op {
|
||||
Connector* ctor_ = nullptr;
|
||||
Stream* stream = nullptr;
|
||||
asio::ip::tcp::resolver::results_type const* res_ = nullptr;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, std::array<std::size_t, 2> const& order = {}
|
||||
, system::error_code const& ec1 = {}
|
||||
, asio::ip::tcp::endpoint const& ep= {}
|
||||
, system::error_code const& ec2 = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
ctor_->timer_.expires_after(ctor_->timeout_);
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token)
|
||||
{
|
||||
auto f = [](system::error_code const&, auto const&) { return true; };
|
||||
return asio::async_connect(*stream, *res_, f, token);
|
||||
},
|
||||
[this](auto token) { return ctor_->timer_.async_wait(token);}
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_one(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (order[0]) {
|
||||
case 0: {
|
||||
ctor_->endpoint_ = ep;
|
||||
self.complete(ec1);
|
||||
} break;
|
||||
case 1:
|
||||
{
|
||||
if (ec2) {
|
||||
self.complete(ec2);
|
||||
} else {
|
||||
self.complete(error::connect_timeout);
|
||||
}
|
||||
} break;
|
||||
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Executor>
|
||||
class connector {
|
||||
public:
|
||||
using timer_type =
|
||||
asio::basic_waitable_timer<
|
||||
std::chrono::steady_clock,
|
||||
asio::wait_traits<std::chrono::steady_clock>,
|
||||
Executor>;
|
||||
|
||||
connector(Executor ex)
|
||||
: timer_{ex}
|
||||
{}
|
||||
|
||||
void set_config(config const& cfg)
|
||||
{ timeout_ = cfg.connect_timeout; }
|
||||
|
||||
template <class Stream, class CompletionToken>
|
||||
auto
|
||||
async_connect(
|
||||
Stream& stream,
|
||||
asio::ip::tcp::resolver::results_type const& res,
|
||||
CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(connect_op<connector, Stream>{this, &stream, &res}, token, timer_);
|
||||
}
|
||||
|
||||
std::size_t cancel(operation op)
|
||||
{
|
||||
switch (op) {
|
||||
case operation::connect:
|
||||
case operation::all:
|
||||
timer_.cancel();
|
||||
break;
|
||||
default: /* ignore */;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto const& endpoint() const noexcept { return endpoint_;}
|
||||
|
||||
private:
|
||||
template <class, class> friend struct connect_op;
|
||||
|
||||
timer_type timer_;
|
||||
std::chrono::steady_clock::duration timeout_ = std::chrono::seconds{2};
|
||||
asio::ip::tcp::endpoint endpoint_;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
36
include/boost/redis/detail/coroutine.hpp
Normal file
36
include/boost/redis/detail/coroutine.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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_DETAIL_COROUTINE_HPP
|
||||
#define BOOST_REDIS_DETAIL_COROUTINE_HPP
|
||||
|
||||
// asio::coroutine uses __COUNTER__ internally, which can trigger
|
||||
// ODR violations if we use them in header-only code. These manifest as
|
||||
// extremely hard-to-debug bugs only present in release builds.
|
||||
// Use this instead when doing coroutines in non-template code.
|
||||
// Adapted from Boost.MySQL.
|
||||
|
||||
// Coroutine state is represented as an integer (resume_point_var).
|
||||
// Every yield gets assigned a unique value (resume_point_id).
|
||||
// Yielding sets the next resume point, returns, and sets a case label for re-entering.
|
||||
// Coroutines need to switch on resume_point_var to re-enter.
|
||||
|
||||
// Enclosing this in a scope allows placing the macro inside a brace-less for/while loop
|
||||
// The empty scope after the case label is required because labels can't be at the end of a compound statement
|
||||
#define BOOST_REDIS_YIELD(resume_point_var, resume_point_id, ...) \
|
||||
{ \
|
||||
resume_point_var = resume_point_id; \
|
||||
return {__VA_ARGS__}; \
|
||||
case resume_point_id: \
|
||||
{ \
|
||||
} \
|
||||
}
|
||||
|
||||
#define BOOST_REDIS_CORO_INITIAL case 0:
|
||||
|
||||
#endif
|
||||
72
include/boost/redis/detail/exec_fsm.hpp
Normal file
72
include/boost/redis/detail/exec_fsm.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// 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_FSM_HPP
|
||||
#define BOOST_REDIS_EXEC_FSM_HPP
|
||||
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
// Sans-io algorithm for async_exec, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// What should we do next?
|
||||
enum class exec_action_type
|
||||
{
|
||||
setup_cancellation, // Set up the cancellation types supported by the composed operation
|
||||
immediate, // Invoke asio::async_immediate to avoid re-entrancy problems
|
||||
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 {
|
||||
exec_action_type type_;
|
||||
system::error_code ec_;
|
||||
std::size_t bytes_read_;
|
||||
|
||||
public:
|
||||
exec_action(exec_action_type type) noexcept
|
||||
: type_{type}
|
||||
{ }
|
||||
|
||||
exec_action(system::error_code ec, std::size_t bytes_read = 0u) noexcept
|
||||
: type_{exec_action_type::done}
|
||||
, ec_{ec}
|
||||
, bytes_read_{bytes_read}
|
||||
{ }
|
||||
|
||||
exec_action_type type() const { return type_; }
|
||||
system::error_code error() const { return ec_; }
|
||||
std::size_t bytes_read() const { return bytes_read_; }
|
||||
};
|
||||
|
||||
class exec_fsm {
|
||||
int resume_point_{0};
|
||||
multiplexer* mpx_{nullptr};
|
||||
std::shared_ptr<multiplexer::elem> elem_;
|
||||
|
||||
public:
|
||||
exec_fsm(multiplexer& mpx, std::shared_ptr<multiplexer::elem> elem) noexcept
|
||||
: mpx_(&mpx)
|
||||
, elem_(std::move(elem))
|
||||
{ }
|
||||
|
||||
exec_action resume(bool connection_is_open, asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -1,124 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_SSL_CONNECTOR_HPP
|
||||
#define BOOST_REDIS_SSL_CONNECTOR_HPP
|
||||
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
namespace boost::redis::detail
|
||||
{
|
||||
|
||||
template <class Handshaker, class Stream>
|
||||
struct handshake_op {
|
||||
Handshaker* hsher_ = nullptr;
|
||||
Stream* stream_ = nullptr;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, std::array<std::size_t, 2> const& order = {}
|
||||
, system::error_code const& ec1 = {}
|
||||
, system::error_code const& ec2 = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
hsher_->timer_.expires_after(hsher_->timeout_);
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token) { return stream_->async_handshake(asio::ssl::stream_base::client, token); },
|
||||
[this](auto token) { return hsher_->timer_.async_wait(token);}
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_one(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (order[0]) {
|
||||
case 0: {
|
||||
self.complete(ec1);
|
||||
} break;
|
||||
case 1:
|
||||
{
|
||||
if (ec2) {
|
||||
self.complete(ec2);
|
||||
} else {
|
||||
self.complete(error::ssl_handshake_timeout);
|
||||
}
|
||||
} break;
|
||||
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Executor>
|
||||
class handshaker {
|
||||
public:
|
||||
using timer_type =
|
||||
asio::basic_waitable_timer<
|
||||
std::chrono::steady_clock,
|
||||
asio::wait_traits<std::chrono::steady_clock>,
|
||||
Executor>;
|
||||
|
||||
handshaker(Executor ex)
|
||||
: timer_{ex}
|
||||
{}
|
||||
|
||||
template <class Stream, class CompletionToken>
|
||||
auto
|
||||
async_handshake(Stream& stream, CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(handshake_op<handshaker, Stream>{this, &stream}, token, timer_);
|
||||
}
|
||||
|
||||
std::size_t cancel(operation op)
|
||||
{
|
||||
switch (op) {
|
||||
case operation::ssl_handshake:
|
||||
case operation::all:
|
||||
timer_.cancel();
|
||||
break;
|
||||
default: /* ignore */;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr bool is_dummy() const noexcept
|
||||
{return false;}
|
||||
|
||||
void set_config(config const& cfg)
|
||||
{ timeout_ = cfg.ssl_handshake_timeout; }
|
||||
|
||||
private:
|
||||
template <class, class> friend struct handshake_op;
|
||||
|
||||
timer_type timer_;
|
||||
std::chrono::steady_clock::duration timeout_;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_SSL_CONNECTOR_HPP
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,49 +7,71 @@
|
||||
#ifndef BOOST_REDIS_HEALTH_CHECKER_HPP
|
||||
#define BOOST_REDIS_HEALTH_CHECKER_HPP
|
||||
|
||||
// Has to included before promise.hpp to build on msvc.
|
||||
#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/redis/operation.hpp>
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <memory>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
template <class HealthChecker, class Connection>
|
||||
template <class HealthChecker, class ConnectionImpl>
|
||||
class ping_op {
|
||||
public:
|
||||
HealthChecker* checker_ = nullptr;
|
||||
Connection* conn_ = nullptr;
|
||||
ConnectionImpl* conn_ = nullptr;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
|
||||
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_, checker_->resp_, std::move(self));
|
||||
BOOST_REDIS_CHECK_OP0(checker_->wait_timer_.cancel();)
|
||||
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));
|
||||
BOOST_REDIS_CHECK_OP0(;)
|
||||
if (ec) {
|
||||
conn_->logger_.trace("ping_op (4)", ec);
|
||||
self.complete(ec);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -64,19 +86,35 @@ public:
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
|
||||
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));
|
||||
BOOST_REDIS_CHECK_OP0(;)
|
||||
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;
|
||||
@@ -91,60 +129,13 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
template <class HealthChecker, class Connection>
|
||||
class check_health_op {
|
||||
public:
|
||||
HealthChecker* checker_ = nullptr;
|
||||
Connection* conn_ = nullptr;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
template <class Self>
|
||||
void
|
||||
operator()(
|
||||
Self& self,
|
||||
std::array<std::size_t, 2> order = {},
|
||||
system::error_code ec1 = {},
|
||||
system::error_code ec2 = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
if (checker_->ping_interval_ == std::chrono::seconds::zero()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::post(std::move(self));
|
||||
self.complete({});
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token) { return checker_->async_ping(*conn_, token); },
|
||||
[this](auto token) { return checker_->async_check_timeout(*conn_, token);}
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_one(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (order[0]) {
|
||||
case 0: self.complete(ec1); return;
|
||||
case 1: self.complete(ec2); return;
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>;
|
||||
using timer_type = asio::basic_waitable_timer<
|
||||
std::chrono::steady_clock,
|
||||
asio::wait_traits<std::chrono::steady_clock>,
|
||||
Executor>;
|
||||
|
||||
public:
|
||||
health_checker(Executor ex)
|
||||
@@ -161,55 +152,36 @@ public:
|
||||
ping_interval_ = cfg.health_check_interval;
|
||||
}
|
||||
|
||||
template <
|
||||
class Connection,
|
||||
class CompletionToken = asio::default_completion_token_t<Executor>
|
||||
>
|
||||
auto async_check_health(Connection& conn, CompletionToken token = CompletionToken{})
|
||||
void cancel()
|
||||
{
|
||||
checker_has_exited_ = false;
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(check_health_op<health_checker, Connection>{this, &conn}, token, conn);
|
||||
ping_timer_.cancel();
|
||||
wait_timer_.cancel();
|
||||
}
|
||||
|
||||
std::size_t cancel(operation op)
|
||||
template <class ConnectionImpl, class CompletionToken>
|
||||
auto async_ping(ConnectionImpl& conn, CompletionToken token)
|
||||
{
|
||||
switch (op) {
|
||||
case operation::health_check:
|
||||
case operation::all:
|
||||
ping_timer_.cancel();
|
||||
wait_timer_.cancel();
|
||||
break;
|
||||
default: /* ignore */;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
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_);
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
ping_op<health_checker, ConnectionImpl>{this, &conn},
|
||||
token,
|
||||
conn,
|
||||
ping_timer_);
|
||||
}
|
||||
|
||||
template <class Connection, class CompletionToken>
|
||||
auto async_check_timeout(Connection& conn, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(check_timeout_op<health_checker, Connection>{this, &conn}, token, conn, wait_timer_);
|
||||
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;
|
||||
template <class, class> friend class check_health_op;
|
||||
|
||||
timer_type ping_timer_;
|
||||
timer_type wait_timer_;
|
||||
@@ -219,6 +191,6 @@ private:
|
||||
bool checker_has_exited_ = false;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_HEALTH_CHECKER_HPP
|
||||
#endif // BOOST_REDIS_HEALTH_CHECKER_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -9,8 +9,7 @@
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
|
||||
namespace boost::redis::detail
|
||||
{
|
||||
namespace boost::redis::detail {
|
||||
|
||||
template <class T>
|
||||
auto is_cancelled(T const& self)
|
||||
@@ -18,20 +17,18 @@ 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_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;\
|
||||
#define BOOST_REDIS_CHECK_OP1(X) \
|
||||
if (ec || redis::detail::is_cancelled(self)) { \
|
||||
X self.complete(!!ec ? ec : asio::error::operation_aborted, {}); \
|
||||
return; \
|
||||
}
|
||||
|
||||
} // boost::redis::detail
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_HELPER_HPP
|
||||
#endif // BOOST_REDIS_HELPER_HPP
|
||||
|
||||
198
include/boost/redis/detail/multiplexer.hpp
Normal file
198
include/boost/redis/detail/multiplexer.hpp
Normal file
@@ -0,0 +1,198 @@
|
||||
/* 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_MULTIPLEXER_HPP
|
||||
#define BOOST_REDIS_MULTIPLEXER_HPP
|
||||
|
||||
#include <boost/redis/adapter/adapt.hpp>
|
||||
#include <boost/redis/adapter/any_adapter.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/system/error_code.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
class request;
|
||||
|
||||
namespace detail {
|
||||
|
||||
using tribool = std::optional<bool>;
|
||||
|
||||
class multiplexer {
|
||||
public:
|
||||
struct elem {
|
||||
public:
|
||||
explicit elem(request const& req, any_adapter adapter);
|
||||
|
||||
void set_done_callback(std::function<void()> f) noexcept { done_ = std::move(f); };
|
||||
|
||||
auto notify_done() noexcept -> void
|
||||
{
|
||||
status_ = status::done;
|
||||
done_();
|
||||
}
|
||||
|
||||
auto notify_error(system::error_code ec) noexcept -> void;
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_waiting() const noexcept
|
||||
{
|
||||
return status_ == status::waiting;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_written() const noexcept
|
||||
{
|
||||
return status_ == status::written;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_staged() const noexcept
|
||||
{
|
||||
return status_ == status::staged;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool is_done() const noexcept
|
||||
{
|
||||
return status_ == status::done;
|
||||
}
|
||||
|
||||
void mark_written() noexcept { status_ = status::written; }
|
||||
|
||||
void mark_staged() noexcept { status_ = status::staged; }
|
||||
|
||||
void mark_waiting() noexcept { status_ = status::waiting; }
|
||||
|
||||
auto get_error() const -> system::error_code const& { return ec_; }
|
||||
|
||||
auto get_request() const -> request const& { return *req_; }
|
||||
|
||||
auto get_read_size() const -> std::size_t { return read_size_; }
|
||||
|
||||
auto get_remaining_responses() const -> std::size_t { return remaining_responses_; }
|
||||
|
||||
auto commit_response(std::size_t read_size) -> void;
|
||||
|
||||
auto get_adapter() -> any_adapter& { return adapter_; }
|
||||
|
||||
private:
|
||||
enum class status
|
||||
{
|
||||
waiting, // the request hasn't been written yet
|
||||
staged, // we've issued the write for this request, but it hasn't finished yet
|
||||
written, // the request has been written successfully
|
||||
done, // the request has completed and the done callback has been invoked
|
||||
};
|
||||
|
||||
request const* req_;
|
||||
any_adapter adapter_;
|
||||
std::function<void()> done_;
|
||||
|
||||
// Contains the number of commands that haven't been read yet.
|
||||
std::size_t remaining_responses_;
|
||||
status status_;
|
||||
|
||||
system::error_code ec_;
|
||||
std::size_t read_size_;
|
||||
};
|
||||
|
||||
auto remove(std::shared_ptr<elem> const& ptr) -> bool;
|
||||
|
||||
[[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;
|
||||
|
||||
// If the tribool contains no value more data is needed, otherwise
|
||||
// if the value is true the message consumed is a push.
|
||||
[[nodiscard]]
|
||||
auto consume_next(std::string_view data, system::error_code& ec)
|
||||
-> std::pair<tribool, std::size_t>;
|
||||
|
||||
auto add(std::shared_ptr<elem> const& ptr) -> void;
|
||||
auto reset() -> void;
|
||||
|
||||
[[nodiscard]]
|
||||
auto const& get_parser() const noexcept
|
||||
{
|
||||
return parser_;
|
||||
}
|
||||
|
||||
//[[nodiscard]]
|
||||
auto cancel_waiting() -> std::size_t;
|
||||
|
||||
//[[nodiscard]]
|
||||
auto cancel_on_conn_lost() -> std::size_t;
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_cancel_run_state() const noexcept -> bool
|
||||
{
|
||||
return cancel_run_called_;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_write_buffer() noexcept -> std::string_view
|
||||
{
|
||||
return std::string_view{write_buffer_};
|
||||
}
|
||||
|
||||
void set_receive_adapter(any_adapter adapter);
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_usage() const noexcept -> usage
|
||||
{
|
||||
return usage_;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_writing() const noexcept -> bool;
|
||||
|
||||
private:
|
||||
[[nodiscard]]
|
||||
auto is_waiting_response() const noexcept -> bool;
|
||||
|
||||
void commit_usage(bool is_push, std::size_t size);
|
||||
|
||||
[[nodiscard]]
|
||||
auto is_next_push(std::string_view data) const noexcept -> bool;
|
||||
|
||||
// Releases the number of requests that have been released.
|
||||
[[nodiscard]]
|
||||
auto release_push_requests() -> std::size_t;
|
||||
|
||||
[[nodiscard]]
|
||||
tribool consume_next_impl(std::string_view data, system::error_code& ec);
|
||||
|
||||
std::string write_buffer_;
|
||||
std::deque<std::shared_ptr<elem>> reqs_;
|
||||
resp3::parser parser_{};
|
||||
bool on_push_ = false;
|
||||
bool cancel_run_called_ = false;
|
||||
usage usage_;
|
||||
any_adapter receive_adapter_;
|
||||
};
|
||||
|
||||
auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>;
|
||||
|
||||
} // namespace detail
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_MULTIPLEXER_HPP
|
||||
@@ -1,291 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_READ_HPP
|
||||
#define BOOST_REDIS_READ_HPP
|
||||
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
#include <boost/redis/adapter/ignore.hpp>
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/asio/read.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <limits>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
template <class DynamicBuffer>
|
||||
std::string_view buffer_view(DynamicBuffer buf) noexcept
|
||||
{
|
||||
char const* start = static_cast<char const*>(buf.data(0, buf.size()).data());
|
||||
return std::string_view{start, std::size(buf)};
|
||||
}
|
||||
|
||||
template <class AsyncReadStream, class DynamicBuffer>
|
||||
class append_some_op {
|
||||
private:
|
||||
AsyncReadStream& stream_;
|
||||
DynamicBuffer buf_;
|
||||
std::size_t size_ = 0;
|
||||
std::size_t tmp_ = 0;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
public:
|
||||
append_some_op(AsyncReadStream& stream, DynamicBuffer buf, std::size_t size)
|
||||
: stream_ {stream}
|
||||
, buf_ {std::move(buf)}
|
||||
, size_{size}
|
||||
{ }
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t n = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
tmp_ = buf_.size();
|
||||
buf_.grow(size_);
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
stream_.async_read_some(buf_.data(tmp_, size_), std::move(self));
|
||||
if (ec) {
|
||||
self.complete(ec, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
buf_.shrink(buf_.size() - tmp_ - n);
|
||||
self.complete({}, n);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class AsyncReadStream, class DynamicBuffer, class CompletionToken>
|
||||
auto
|
||||
async_append_some(
|
||||
AsyncReadStream& stream,
|
||||
DynamicBuffer buffer,
|
||||
std::size_t size,
|
||||
CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code, std::size_t)
|
||||
>(append_some_op<AsyncReadStream, DynamicBuffer> {stream, buffer, size}, token, stream);
|
||||
}
|
||||
|
||||
template <
|
||||
class AsyncReadStream,
|
||||
class DynamicBuffer,
|
||||
class ResponseAdapter>
|
||||
class parse_op {
|
||||
private:
|
||||
AsyncReadStream& stream_;
|
||||
DynamicBuffer buf_;
|
||||
resp3::parser parser_;
|
||||
ResponseAdapter adapter_;
|
||||
bool needs_rescheduling_ = true;
|
||||
system::error_code ec_;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
static std::size_t const growth = 1024;
|
||||
|
||||
public:
|
||||
parse_op(AsyncReadStream& stream, DynamicBuffer buf, ResponseAdapter adapter)
|
||||
: stream_ {stream}
|
||||
, buf_ {std::move(buf)}
|
||||
, adapter_ {std::move(adapter)}
|
||||
{ }
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, system::error_code ec = {}
|
||||
, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
while (!resp3::parse(parser_, buffer_view(buf_), adapter_, ec)) {
|
||||
needs_rescheduling_ = false;
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
async_append_some(
|
||||
stream_, buf_, parser_.get_suggested_buffer_growth(growth),
|
||||
std::move(self));
|
||||
if (ec) {
|
||||
self.complete(ec, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ec_ = ec;
|
||||
if (needs_rescheduling_) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::post(std::move(self));
|
||||
}
|
||||
|
||||
self.complete(ec_, parser_.get_consumed());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** \brief Reads a complete response to a command sychronously.
|
||||
*
|
||||
* This function reads a complete response to a command or a
|
||||
* server push synchronously. For example
|
||||
*
|
||||
* @code
|
||||
* int resp;
|
||||
* std::string buffer;
|
||||
* resp3::read(socket, dynamic_buffer(buffer), adapt(resp));
|
||||
* @endcode
|
||||
*
|
||||
* For a complete example see examples/intro_sync.cpp. This function
|
||||
* is implemented in terms of one or more calls to @c
|
||||
* asio::read_until and @c asio::read functions, and is known as a @a
|
||||
* composed @a operation. Furthermore, the implementation may read
|
||||
* additional bytes from the stream that lie past the end of the
|
||||
* message being read. These additional bytes are stored in the
|
||||
* dynamic buffer, which must be preserved for subsequent reads.
|
||||
*
|
||||
* \param stream The stream from which to read e.g. a tcp socket.
|
||||
* \param buf Dynamic buffer (version 2).
|
||||
* \param adapter The response adapter.
|
||||
* \param ec If an error occurs, it will be assigned to this paramter.
|
||||
* \returns The number of bytes that have been consumed from the dynamic buffer.
|
||||
*
|
||||
* \remark This function calls buf.consume() in each chunk of data
|
||||
* after it has been passed to the adapter. Users must not consume
|
||||
* the bytes after it returns.
|
||||
*/
|
||||
template <
|
||||
class SyncReadStream,
|
||||
class DynamicBuffer,
|
||||
class ResponseAdapter
|
||||
>
|
||||
auto
|
||||
read(
|
||||
SyncReadStream& stream,
|
||||
DynamicBuffer buf,
|
||||
ResponseAdapter adapter,
|
||||
system::error_code& ec) -> std::size_t
|
||||
{
|
||||
static std::size_t const growth = 1024;
|
||||
|
||||
resp3::parser parser;
|
||||
while (!parser.done()) {
|
||||
auto const res = parser.consume(detail::buffer_view(buf), ec);
|
||||
if (ec)
|
||||
return 0UL;
|
||||
|
||||
if (!res.has_value()) {
|
||||
auto const size_before = buf.size();
|
||||
buf.grow(parser.get_suggested_buffer_growth(growth));
|
||||
auto const n =
|
||||
stream.read_some(
|
||||
buf.data(size_before, parser.get_suggested_buffer_growth(growth)),
|
||||
ec);
|
||||
if (ec)
|
||||
return 0UL;
|
||||
|
||||
buf.shrink(buf.size() - size_before - n);
|
||||
continue;
|
||||
}
|
||||
|
||||
adapter(res.value(), ec);
|
||||
if (ec)
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
return parser.get_consumed();
|
||||
}
|
||||
|
||||
/** \brief Reads a complete response to a command sychronously.
|
||||
*
|
||||
* Same as the error_code overload but throws on error.
|
||||
*/
|
||||
template<
|
||||
class SyncReadStream,
|
||||
class DynamicBuffer,
|
||||
class ResponseAdapter = adapter::ignore>
|
||||
auto
|
||||
read(
|
||||
SyncReadStream& stream,
|
||||
DynamicBuffer buf,
|
||||
ResponseAdapter adapter = ResponseAdapter{})
|
||||
{
|
||||
system::error_code ec;
|
||||
auto const n = redis::detail::read(stream, buf, adapter, ec);
|
||||
|
||||
if (ec)
|
||||
BOOST_THROW_EXCEPTION(system::system_error{ec});
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
/** \brief Reads a complete response to a Redis command asynchronously.
|
||||
*
|
||||
* This function reads a complete response to a command or a
|
||||
* server push asynchronously. For example
|
||||
*
|
||||
* @code
|
||||
* std::string buffer;
|
||||
* std::set<std::string> resp;
|
||||
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(resp));
|
||||
* @endcode
|
||||
*
|
||||
* For a complete example see examples/transaction.cpp. This function
|
||||
* is implemented in terms of one or more calls to @c
|
||||
* asio::async_read_until and @c asio::async_read functions, and is
|
||||
* known as a @a composed @a operation. Furthermore, the
|
||||
* implementation may read additional bytes from the stream that lie
|
||||
* past the end of the message being read. These additional bytes are
|
||||
* stored in the dynamic buffer, which must be preserved for
|
||||
* subsequent reads.
|
||||
*
|
||||
* \param stream The stream from which to read e.g. a tcp socket.
|
||||
* \param buffer Dynamic buffer (version 2).
|
||||
* \param adapter The response adapter.
|
||||
* \param token The completion token.
|
||||
*
|
||||
* The completion handler will receive as a parameter the total
|
||||
* number of bytes transferred from the stream and must have the
|
||||
* following signature
|
||||
*
|
||||
* @code
|
||||
* void(system::error_code, std::size_t);
|
||||
* @endcode
|
||||
*
|
||||
* \remark This function calls buf.consume() in each chunk of data
|
||||
* after it has been passed to the adapter. Users must not consume
|
||||
* the bytes after it returns.
|
||||
*/
|
||||
template <
|
||||
class AsyncReadStream,
|
||||
class DynamicBuffer,
|
||||
class ResponseAdapter = adapter::ignore,
|
||||
class CompletionToken = asio::default_completion_token_t<typename AsyncReadStream::executor_type>
|
||||
>
|
||||
auto async_read(
|
||||
AsyncReadStream& stream,
|
||||
DynamicBuffer buffer,
|
||||
ResponseAdapter adapter = ResponseAdapter{},
|
||||
CompletionToken&& token =
|
||||
asio::default_completion_token_t<typename AsyncReadStream::executor_type>{})
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code, std::size_t)
|
||||
>(parse_op<AsyncReadStream, DynamicBuffer, ResponseAdapter> {stream, buffer, adapter},
|
||||
token,
|
||||
stream);
|
||||
}
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_READ_HPP
|
||||
65
include/boost/redis/detail/read_buffer.hpp
Normal file
65
include/boost/redis/detail/read_buffer.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/* 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>;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
[[nodiscard]]
|
||||
auto prepare_append() -> system::error_code;
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_append_buffer() noexcept -> span_type;
|
||||
|
||||
void commit_append(std::size_t read_size);
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_committed_buffer() const noexcept -> std::string_view;
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_committed_size() const noexcept -> std::size_t;
|
||||
|
||||
void clear();
|
||||
|
||||
// Consume committed data.
|
||||
auto consume_committed(std::size_t size) -> std::size_t;
|
||||
|
||||
void reserve(std::size_t n);
|
||||
|
||||
friend bool operator==(read_buffer const& lhs, read_buffer const& rhs);
|
||||
|
||||
friend bool operator!=(read_buffer const& lhs, read_buffer const& rhs);
|
||||
|
||||
void set_config(config const& cfg) noexcept { cfg_ = cfg; };
|
||||
|
||||
private:
|
||||
config cfg_ = config{};
|
||||
std::vector<char> buffer_;
|
||||
std::size_t append_buf_begin_ = 0;
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_READ_BUFFER_HPP
|
||||
56
include/boost/redis/detail/reader_fsm.hpp
Normal file
56
include/boost/redis/detail/reader_fsm.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
/* 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_READER_FSM_HPP
|
||||
#define BOOST_REDIS_READER_FSM_HPP
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
class read_buffer;
|
||||
|
||||
class reader_fsm {
|
||||
public:
|
||||
struct action {
|
||||
enum class type
|
||||
{
|
||||
setup_cancellation,
|
||||
append_some,
|
||||
needs_more,
|
||||
notify_push_receiver,
|
||||
cancel_run,
|
||||
done,
|
||||
};
|
||||
|
||||
type type_ = type::setup_cancellation;
|
||||
std::size_t push_size_ = 0u;
|
||||
system::error_code ec_ = {};
|
||||
};
|
||||
|
||||
explicit reader_fsm(read_buffer& rbuf, multiplexer& mpx) noexcept;
|
||||
|
||||
action resume(
|
||||
std::size_t bytes_read,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t /*cancel_state*/);
|
||||
|
||||
private:
|
||||
int resume_point_{0};
|
||||
read_buffer* read_buffer_ = nullptr;
|
||||
action action_after_resume_;
|
||||
action::type next_read_type_ = action::type::append_some;
|
||||
multiplexer* mpx_ = nullptr;
|
||||
std::pair<tribool, std::size_t> res_{std::make_pair(std::nullopt, 0)};
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_READER_FSM_HPP
|
||||
291
include/boost/redis/detail/redis_stream.hpp
Normal file
291
include/boost/redis/detail/redis_stream.hpp
Normal file
@@ -0,0 +1,291 @@
|
||||
/* Copyright (c) 2018-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.txt)
|
||||
*/
|
||||
#ifndef BOOST_REDIS_REDIS_STREAM_HPP
|
||||
#define BOOST_REDIS_REDIS_STREAM_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_logger.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
|
||||
#include <boost/asio/basic_waitable_timer.hpp>
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/ip/basic_resolver.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/local/stream_protocol.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <utility>
|
||||
|
||||
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_;
|
||||
asio::ip::basic_resolver<asio::ip::tcp, Executor> resolv_;
|
||||
asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp, Executor>> stream_;
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
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};
|
||||
|
||||
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{};
|
||||
|
||||
// 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 operator()(
|
||||
Self& self,
|
||||
system::error_code ec,
|
||||
const asio::ip::tcp::endpoint& selected_endpoint)
|
||||
{
|
||||
lgr->on_connect(ec, selected_endpoint);
|
||||
(*this)(self, ec);
|
||||
}
|
||||
|
||||
template <class Self>
|
||||
void operator()(
|
||||
Self& self,
|
||||
system::error_code ec = {},
|
||||
asio::ip::tcp::resolver::results_type resolver_results = {})
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
explicit redis_stream(Executor ex, asio::ssl::context&& ssl_ctx)
|
||||
: ssl_ctx_{std::move(ssl_ctx)}
|
||||
, resolv_{ex}
|
||||
, stream_{ex, ssl_ctx_}
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
, unix_socket_{ex}
|
||||
#endif
|
||||
, timer_{std::move(ex)}
|
||||
{ }
|
||||
|
||||
// Executor. Required to satisfy the AsyncStream concept
|
||||
using executor_type = Executor;
|
||||
executor_type get_executor() noexcept { return resolv_.get_executor(); }
|
||||
|
||||
// Accessors
|
||||
const auto& get_ssl_context() const noexcept { return ssl_ctx_; }
|
||||
bool is_open() const
|
||||
{
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
if (transport_ == transport_type::unix_socket)
|
||||
return unix_socket_.is_open();
|
||||
#endif
|
||||
return stream_.next_layer().is_open();
|
||||
}
|
||||
auto& next_layer() { return stream_; }
|
||||
const auto& next_layer() const { return stream_; }
|
||||
|
||||
// I/O
|
||||
template <class CompletionToken>
|
||||
auto async_connect(const config* cfg, connection_logger* l, CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
connect_op{*this, cfg, l},
|
||||
token);
|
||||
}
|
||||
|
||||
// These functions should only be used with callbacks (e.g. within async_compose function bodies)
|
||||
template <class ConstBufferSequence, class CompletionToken>
|
||||
void async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token)
|
||||
{
|
||||
switch (transport_) {
|
||||
case transport_type::tcp:
|
||||
{
|
||||
stream_.next_layer().async_write_some(buffers, std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
case transport_type::tcp_tls:
|
||||
{
|
||||
stream_.async_write_some(buffers, std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
case transport_type::unix_socket:
|
||||
{
|
||||
unix_socket_.async_write_some(buffers, std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
|
||||
template <class MutableBufferSequence, class CompletionToken>
|
||||
void async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token)
|
||||
{
|
||||
switch (transport_) {
|
||||
case transport_type::tcp:
|
||||
{
|
||||
return stream_.next_layer().async_read_some(
|
||||
buffers,
|
||||
std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
case transport_type::tcp_tls:
|
||||
{
|
||||
return stream_.async_read_some(buffers, std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
case transport_type::unix_socket:
|
||||
{
|
||||
unix_socket_.async_read_some(buffers, std::forward<CompletionToken>(token));
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
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
|
||||
} // namespace redis
|
||||
} // namespace boost
|
||||
|
||||
#endif
|
||||
@@ -1,137 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_RESOLVER_HPP
|
||||
#define BOOST_REDIS_RESOLVER_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
namespace boost::redis::detail
|
||||
{
|
||||
|
||||
template <class Resolver>
|
||||
struct resolve_op {
|
||||
Resolver* resv_ = nullptr;
|
||||
asio::coroutine coro{};
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, std::array<std::size_t, 2> order = {}
|
||||
, system::error_code ec1 = {}
|
||||
, asio::ip::tcp::resolver::results_type res = {}
|
||||
, system::error_code ec2 = {})
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro)
|
||||
{
|
||||
resv_->timer_.expires_after(resv_->timeout_);
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token)
|
||||
{
|
||||
return resv_->resv_.async_resolve(resv_->addr_.host, resv_->addr_.port, token);
|
||||
},
|
||||
[this](auto token) { return resv_->timer_.async_wait(token);}
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_one(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (order[0]) {
|
||||
case 0: {
|
||||
// Resolver completed first.
|
||||
resv_->results_ = res;
|
||||
self.complete(ec1);
|
||||
} break;
|
||||
|
||||
case 1: {
|
||||
if (ec2) {
|
||||
// Timer completed first with error, perhaps a
|
||||
// cancellation going on.
|
||||
self.complete(ec2);
|
||||
} else {
|
||||
// Timer completed first without an error, this is a
|
||||
// resolve timeout.
|
||||
self.complete(error::resolve_timeout);
|
||||
}
|
||||
} break;
|
||||
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Executor>
|
||||
class resolver {
|
||||
public:
|
||||
using timer_type =
|
||||
asio::basic_waitable_timer<
|
||||
std::chrono::steady_clock,
|
||||
asio::wait_traits<std::chrono::steady_clock>,
|
||||
Executor>;
|
||||
|
||||
resolver(Executor ex) : resv_{ex} , timer_{ex} {}
|
||||
|
||||
template <class CompletionToken>
|
||||
auto async_resolve(CompletionToken&& token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(resolve_op<resolver>{this}, token, resv_);
|
||||
}
|
||||
|
||||
std::size_t cancel(operation op)
|
||||
{
|
||||
switch (op) {
|
||||
case operation::resolve:
|
||||
case operation::all:
|
||||
resv_.cancel();
|
||||
timer_.cancel();
|
||||
break;
|
||||
default: /* ignore */;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto const& results() const noexcept
|
||||
{ return results_;}
|
||||
|
||||
void set_config(config const& cfg)
|
||||
{
|
||||
addr_ = cfg.addr;
|
||||
timeout_ = cfg.resolve_timeout;
|
||||
}
|
||||
|
||||
private:
|
||||
using resolver_type = asio::ip::basic_resolver<asio::ip::tcp, Executor>;
|
||||
template <class> friend struct resolve_op;
|
||||
|
||||
resolver_type resv_;
|
||||
timer_type timer_;
|
||||
address addr_;
|
||||
std::chrono::steady_clock::duration timeout_;
|
||||
asio::ip::tcp::resolver::results_type results_;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_RESOLVER_HPP
|
||||
115
include/boost/redis/detail/resp3_handshaker.hpp
Normal file
115
include/boost/redis/detail/resp3_handshaker.hpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_RUNNER_HPP
|
||||
#define BOOST_REDIS_RUNNER_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_logger.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
void push_hello(config const& cfg, request& req);
|
||||
|
||||
// TODO: Can we avoid this whole function whose only purpose is to
|
||||
// check for an error in the hello response and complete with an error
|
||||
// so that the parallel group that starts it can exit?
|
||||
template <class Handshaker, class ConnectionImpl>
|
||||
struct hello_op {
|
||||
Handshaker* handshaker_ = nullptr;
|
||||
ConnectionImpl* conn_ = nullptr;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER(coro_)
|
||||
{
|
||||
handshaker_->add_hello();
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->async_exec(
|
||||
handshaker_->hello_req_,
|
||||
any_adapter{handshaker_->hello_resp_},
|
||||
std::move(self));
|
||||
|
||||
conn_->logger_.on_hello(ec, handshaker_->hello_resp_);
|
||||
|
||||
if (ec) {
|
||||
conn_->cancel(operation::run);
|
||||
self.complete(ec);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handshaker_->has_error_in_response()) {
|
||||
conn_->cancel(operation::run);
|
||||
self.complete(error::resp3_hello);
|
||||
return;
|
||||
}
|
||||
|
||||
self.complete({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Executor>
|
||||
class resp3_handshaker {
|
||||
public:
|
||||
void set_config(config const& cfg) { cfg_ = cfg; }
|
||||
|
||||
template <class ConnectionImpl, class CompletionToken>
|
||||
auto async_hello(ConnectionImpl& conn, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
hello_op<resp3_handshaker, ConnectionImpl>{this, &conn},
|
||||
token,
|
||||
conn);
|
||||
}
|
||||
|
||||
private:
|
||||
template <class, class> friend struct hello_op;
|
||||
|
||||
void add_hello()
|
||||
{
|
||||
hello_req_.clear();
|
||||
if (hello_resp_.has_value())
|
||||
hello_resp_.value().clear();
|
||||
push_hello(cfg_, hello_req_);
|
||||
}
|
||||
|
||||
bool has_error_in_response() const noexcept
|
||||
{
|
||||
if (!hello_resp_.has_value())
|
||||
return true;
|
||||
|
||||
auto f = [](auto const& e) {
|
||||
switch (e.data_type) {
|
||||
case resp3::type::simple_error:
|
||||
case resp3::type::blob_error: return true;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
return std::any_of(std::cbegin(hello_resp_.value()), std::cend(hello_resp_.value()), f);
|
||||
}
|
||||
|
||||
request hello_req_;
|
||||
generic_response hello_resp_;
|
||||
config cfg_;
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_RUNNER_HPP
|
||||
@@ -1,250 +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)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_RUNNER_HPP
|
||||
#define BOOST_REDIS_RUNNER_HPP
|
||||
|
||||
#include <boost/redis/detail/health_checker.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
#include <boost/redis/detail/helper.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
#include <boost/redis/detail/connector.hpp>
|
||||
#include <boost/redis/detail/resolver.hpp>
|
||||
#include <boost/redis/detail/handshaker.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
|
||||
namespace boost::redis::detail
|
||||
{
|
||||
|
||||
template <class Runner, class Connection, class Logger>
|
||||
struct hello_op {
|
||||
Runner* runner_ = nullptr;
|
||||
Connection* conn_ = nullptr;
|
||||
Logger logger_;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
runner_->hello_req_.clear();
|
||||
if (runner_->hello_resp_.has_value())
|
||||
runner_->hello_resp_.value().clear();
|
||||
runner_->add_hello();
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self));
|
||||
logger_.on_hello(ec, runner_->hello_resp_);
|
||||
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
|
||||
self.complete(ec);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Runner, class Connection, class Logger>
|
||||
class runner_op {
|
||||
private:
|
||||
Runner* runner_ = nullptr;
|
||||
Connection* conn_ = nullptr;
|
||||
Logger logger_;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
public:
|
||||
runner_op(Runner* runner, Connection* conn, Logger l)
|
||||
: runner_{runner}
|
||||
, conn_{conn}
|
||||
, logger_{l}
|
||||
{}
|
||||
|
||||
template <class Self>
|
||||
void operator()( Self& self
|
||||
, std::array<std::size_t, 3> order = {}
|
||||
, system::error_code ec0 = {}
|
||||
, system::error_code ec1 = {}
|
||||
, system::error_code ec2 = {}
|
||||
, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
asio::experimental::make_parallel_group(
|
||||
[this](auto token) { return runner_->async_run_all(*conn_, logger_, token); },
|
||||
[this](auto token) { return runner_->health_checker_.async_check_health(*conn_, token); },
|
||||
[this](auto token) { return runner_->async_hello(*conn_, logger_, token); }
|
||||
).async_wait(
|
||||
asio::experimental::wait_for_all(),
|
||||
std::move(self));
|
||||
|
||||
if (is_cancelled(self)) {
|
||||
self.complete(asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec0 == error::connect_timeout || ec0 == error::resolve_timeout) {
|
||||
self.complete(ec0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (order[0] == 2 && !!ec2) {
|
||||
self.complete(ec2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (order[0] == 1 && ec1 == error::pong_timeout) {
|
||||
self.complete(ec1);
|
||||
return;
|
||||
}
|
||||
|
||||
self.complete(ec0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Runner, class Connection, class Logger>
|
||||
struct run_all_op {
|
||||
Runner* runner_ = nullptr;
|
||||
Connection* conn_ = nullptr;
|
||||
Logger logger_;
|
||||
asio::coroutine coro_{};
|
||||
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
|
||||
{
|
||||
BOOST_ASIO_CORO_REENTER (coro_)
|
||||
{
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
runner_->resv_.async_resolve(std::move(self));
|
||||
logger_.on_resolve(ec, runner_->resv_.results());
|
||||
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
runner_->ctor_.async_connect(conn_->next_layer().next_layer(), runner_->resv_.results(), std::move(self));
|
||||
logger_.on_connect(ec, runner_->ctor_.endpoint());
|
||||
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
|
||||
|
||||
if (conn_->use_ssl()) {
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
runner_->hsher_.async_handshake(conn_->next_layer(), std::move(self));
|
||||
logger_.on_ssl_handshake(ec);
|
||||
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
|
||||
}
|
||||
|
||||
BOOST_ASIO_CORO_YIELD
|
||||
conn_->async_run_lean(runner_->cfg_, logger_, std::move(self));
|
||||
BOOST_REDIS_CHECK_OP0(;)
|
||||
self.complete(ec);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Executor>
|
||||
class runner {
|
||||
public:
|
||||
runner(Executor ex, config cfg)
|
||||
: resv_{ex}
|
||||
, ctor_{ex}
|
||||
, hsher_{ex}
|
||||
, health_checker_{ex}
|
||||
, cfg_{cfg}
|
||||
{ }
|
||||
|
||||
std::size_t cancel(operation op)
|
||||
{
|
||||
resv_.cancel(op);
|
||||
ctor_.cancel(op);
|
||||
hsher_.cancel(op);
|
||||
health_checker_.cancel(op);
|
||||
return 0U;
|
||||
}
|
||||
|
||||
void set_config(config const& cfg)
|
||||
{
|
||||
cfg_ = cfg;
|
||||
resv_.set_config(cfg);
|
||||
ctor_.set_config(cfg);
|
||||
hsher_.set_config(cfg);
|
||||
health_checker_.set_config(cfg);
|
||||
}
|
||||
|
||||
template <class Connection, class Logger, class CompletionToken>
|
||||
auto async_run(Connection& conn, Logger l, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(runner_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
|
||||
}
|
||||
|
||||
config const& get_config() const noexcept {return cfg_;}
|
||||
|
||||
private:
|
||||
using resolver_type = resolver<Executor>;
|
||||
using connector_type = connector<Executor>;
|
||||
using handshaker_type = detail::handshaker<Executor>;
|
||||
using health_checker_type = health_checker<Executor>;
|
||||
using timer_type = typename connector_type::timer_type;
|
||||
|
||||
template <class, class, class> friend struct run_all_op;
|
||||
template <class, class, class> friend class runner_op;
|
||||
template <class, class, class> friend struct hello_op;
|
||||
|
||||
template <class Connection, class Logger, class CompletionToken>
|
||||
auto async_run_all(Connection& conn, Logger l, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(run_all_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
|
||||
}
|
||||
|
||||
template <class Connection, class Logger, class CompletionToken>
|
||||
auto async_hello(Connection& conn, Logger l, CompletionToken token)
|
||||
{
|
||||
return asio::async_compose
|
||||
< CompletionToken
|
||||
, void(system::error_code)
|
||||
>(hello_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
|
||||
}
|
||||
|
||||
void add_hello()
|
||||
{
|
||||
if (!cfg_.username.empty() && !cfg_.password.empty() && !cfg_.clientname.empty())
|
||||
hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password, "SETNAME", cfg_.clientname);
|
||||
else if (cfg_.username.empty() && cfg_.password.empty() && cfg_.clientname.empty())
|
||||
hello_req_.push("HELLO", "3");
|
||||
else if (cfg_.clientname.empty())
|
||||
hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password);
|
||||
else
|
||||
hello_req_.push("HELLO", "3", "SETNAME", cfg_.clientname);
|
||||
|
||||
if (cfg_.database_index)
|
||||
hello_req_.push("SELECT", cfg_.database_index.value());
|
||||
}
|
||||
|
||||
resolver_type resv_;
|
||||
connector_type ctor_;
|
||||
handshaker_type hsher_;
|
||||
health_checker_type health_checker_;
|
||||
request hello_req_;
|
||||
generic_response hello_resp_;
|
||||
config cfg_;
|
||||
};
|
||||
|
||||
} // boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_RUNNER_HPP
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,40 +7,39 @@
|
||||
#ifndef BOOST_REDIS_WRITE_HPP
|
||||
#define BOOST_REDIS_WRITE_HPP
|
||||
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/asio/write.hpp>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
/** \brief Writes a request synchronously.
|
||||
* \ingroup low-level-api
|
||||
/** @brief Writes a request synchronously.
|
||||
*
|
||||
* \param stream Stream to write the request to.
|
||||
* \param req Request to write.
|
||||
* @param stream Stream to write the request to.
|
||||
* @param req Request to write.
|
||||
*/
|
||||
template<class SyncWriteStream>
|
||||
template <class SyncWriteStream>
|
||||
auto write(SyncWriteStream& stream, request const& req)
|
||||
{
|
||||
return asio::write(stream, asio::buffer(req.payload()));
|
||||
}
|
||||
|
||||
template<class SyncWriteStream>
|
||||
template <class SyncWriteStream>
|
||||
auto write(SyncWriteStream& stream, request const& req, system::error_code& ec)
|
||||
{
|
||||
return asio::write(stream, asio::buffer(req.payload()), ec);
|
||||
}
|
||||
|
||||
/** \brief Writes a request asynchronously.
|
||||
* \ingroup low-level-api
|
||||
/** @brief Writes a request asynchronously.
|
||||
*
|
||||
* \param stream Stream to write the request to.
|
||||
* \param req Request to write.
|
||||
* \param token Asio completion token.
|
||||
* @param stream Stream to write the request to.
|
||||
* @param req Request to write.
|
||||
* @param token Asio completion token.
|
||||
*/
|
||||
template<
|
||||
template <
|
||||
class AsyncWriteStream,
|
||||
class CompletionToken = asio::default_completion_token_t<typename AsyncWriteStream::executor_type>
|
||||
>
|
||||
class CompletionToken =
|
||||
asio::default_completion_token_t<typename AsyncWriteStream::executor_type> >
|
||||
auto async_write(
|
||||
AsyncWriteStream& stream,
|
||||
request const& req,
|
||||
@@ -50,6 +49,6 @@ auto async_write(
|
||||
return asio::async_write(stream, asio::buffer(req.payload()), token);
|
||||
}
|
||||
|
||||
} // boost::redis::detail
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_WRITE_HPP
|
||||
#endif // BOOST_REDIS_WRITE_HPP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -11,9 +11,7 @@
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
/** \brief Generic errors.
|
||||
* \ingroup high-level-api
|
||||
*/
|
||||
/// Generic errors.
|
||||
enum class error
|
||||
{
|
||||
/// Invalid RESP3 type.
|
||||
@@ -75,22 +73,40 @@ enum class error
|
||||
|
||||
/// SSL handshake timeout
|
||||
ssl_handshake_timeout,
|
||||
|
||||
/// Can't receive push synchronously without blocking
|
||||
sync_receive_push_failed,
|
||||
|
||||
/// Incompatible node depth.
|
||||
incompatible_node_depth,
|
||||
|
||||
/// Resp3 hello command error
|
||||
resp3_hello,
|
||||
|
||||
/// The configuration specified a UNIX socket address, but UNIX sockets are not supported by the system.
|
||||
unix_sockets_unsupported,
|
||||
|
||||
/// 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,
|
||||
};
|
||||
|
||||
/** \internal
|
||||
* \brief Creates a error_code object from an error.
|
||||
* \param e Error code.
|
||||
* \ingroup any
|
||||
/**
|
||||
* @brief Creates a error_code object from an error.
|
||||
*
|
||||
* @param e Error code.
|
||||
*/
|
||||
auto make_error_code(error e) -> system::error_code;
|
||||
|
||||
} // boost::redis
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace std {
|
||||
|
||||
template<>
|
||||
struct is_error_code_enum<::boost::redis::error> : std::true_type {};
|
||||
template <>
|
||||
struct is_error_code_enum<::boost::redis::error> : std::true_type { };
|
||||
|
||||
} // std
|
||||
} // namespace std
|
||||
|
||||
#endif // BOOST_REDIS_ERROR_HPP
|
||||
#endif // BOOST_REDIS_ERROR_HPP
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -13,30 +13,27 @@
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace boost::redis
|
||||
{
|
||||
namespace boost::redis {
|
||||
|
||||
/** @brief Type used to ignore responses.
|
||||
* @ingroup high-level-api
|
||||
*
|
||||
* For example
|
||||
* For example:
|
||||
*
|
||||
* @code
|
||||
* response<ignore_t, std::string, ignore_t> resp;
|
||||
* @endcode
|
||||
*
|
||||
* will ignore the first and third responses. RESP3 errors won't be
|
||||
* This will ignore the first and third responses. RESP3 errors won't be
|
||||
* ignore but will cause `async_exec` to complete with an error.
|
||||
*/
|
||||
using ignore_t = std::decay_t<decltype(std::ignore)>;
|
||||
|
||||
/** @brief Global ignore object.
|
||||
* @ingroup high-level-api
|
||||
*
|
||||
* Can be used to ignore responses to a request
|
||||
* Can be used to ignore responses to a request. For example:
|
||||
*
|
||||
* @code
|
||||
* conn->async_exec(req, ignore, ...);
|
||||
* co_await conn.async_exec(req, ignore);
|
||||
* @endcode
|
||||
*
|
||||
* RESP3 errors won't be ignore but will cause `async_exec` to
|
||||
@@ -44,6 +41,6 @@ using ignore_t = std::decay_t<decltype(std::ignore)>;
|
||||
*/
|
||||
extern ignore_t ignore;
|
||||
|
||||
} // boost::redis
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_IGNORE_HPP
|
||||
#endif // BOOST_REDIS_IGNORE_HPP
|
||||
|
||||
@@ -1,39 +1,56 @@
|
||||
/* 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/impl/log_to_file.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
connection::connection(
|
||||
executor_type ex,
|
||||
asio::ssl::context::method method,
|
||||
std::size_t max_read_size)
|
||||
: impl_{ex, method, max_read_size}
|
||||
logger detail::make_stderr_logger(logger::level lvl, std::string prefix)
|
||||
{
|
||||
return logger(lvl, [prefix = std::move(prefix)](logger::level, std::string_view msg) {
|
||||
log_to_file(stderr, msg, prefix.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
connection::connection(executor_type ex, asio::ssl::context ctx, logger lgr)
|
||||
: impl_{std::move(ex), std::move(ctx), std::move(lgr)}
|
||||
{ }
|
||||
|
||||
connection::connection(
|
||||
asio::io_context& ioc,
|
||||
asio::ssl::context::method method,
|
||||
std::size_t max_read_size)
|
||||
: impl_{ioc.get_executor(), method, max_read_size}
|
||||
{ }
|
||||
|
||||
void
|
||||
connection::async_run_impl(
|
||||
void connection::async_run_impl(
|
||||
config const& cfg,
|
||||
logger l,
|
||||
logger&& l,
|
||||
asio::any_completion_handler<void(boost::system::error_code)> token)
|
||||
{
|
||||
impl_.async_run(cfg, l, std::move(token));
|
||||
// Avoid calling the basic_connection::async_run overload taking a logger
|
||||
// because it generates deprecated messages when building this file
|
||||
impl_.set_stderr_logger(l.lvl, cfg);
|
||||
impl_.async_run(cfg, std::move(token));
|
||||
}
|
||||
|
||||
void connection::cancel(operation op)
|
||||
void connection::async_run_impl(
|
||||
config const& cfg,
|
||||
asio::any_completion_handler<void(boost::system::error_code)> token)
|
||||
{
|
||||
impl_.cancel(op);
|
||||
impl_.async_run(cfg, std::move(token));
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
void connection::async_exec_impl(
|
||||
request const& req,
|
||||
any_adapter&& adapter,
|
||||
asio::any_completion_handler<void(boost::system::error_code, std::size_t)> token)
|
||||
{
|
||||
impl_.async_exec(req, std::move(adapter), std::move(token));
|
||||
}
|
||||
|
||||
void connection::cancel(operation op) { impl_.cancel(op); }
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
216
include/boost/redis/impl/connection_logger.ipp
Normal file
216
include/boost/redis/impl/connection_logger.ipp
Normal file
@@ -0,0 +1,216 @@
|
||||
/* 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
|
||||
@@ -1,62 +1,74 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* 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/error.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
namespace boost::redis {
|
||||
namespace detail {
|
||||
|
||||
struct error_category_impl : system::error_category {
|
||||
|
||||
virtual ~error_category_impl() = default;
|
||||
|
||||
auto name() const noexcept -> char const* override
|
||||
{
|
||||
return "boost.redis";
|
||||
}
|
||||
auto name() const noexcept -> char const* override { return "boost.redis"; }
|
||||
|
||||
auto message(int ev) const -> std::string override
|
||||
{
|
||||
switch(static_cast<error>(ev)) {
|
||||
case error::invalid_data_type: return "Invalid resp3 type.";
|
||||
case error::not_a_number: return "Can't convert string to number (maybe forgot to upgrade to RESP3?).";
|
||||
case error::exceeeds_max_nested_depth: return "Exceeds the maximum number of nested responses.";
|
||||
case error::unexpected_bool_value: return "Unexpected bool value.";
|
||||
case error::empty_field: return "Expected field value is empty.";
|
||||
case error::expects_resp3_simple_type: return "Expects a resp3 simple type.";
|
||||
case error::expects_resp3_aggregate: return "Expects resp3 aggregate.";
|
||||
case error::expects_resp3_map: return "Expects resp3 map.";
|
||||
case error::expects_resp3_set: return "Expects resp3 set.";
|
||||
case error::nested_aggregate_not_supported: return "Nested aggregate not_supported.";
|
||||
case error::resp3_simple_error: return "Got RESP3 simple-error.";
|
||||
case error::resp3_blob_error: return "Got RESP3 blob-error.";
|
||||
case error::incompatible_size: return "Aggregate container has incompatible size.";
|
||||
case error::not_a_double: return "Not a double.";
|
||||
case error::resp3_null: return "Got RESP3 null.";
|
||||
case error::not_connected: return "Not connected.";
|
||||
case error::resolve_timeout: return "Resolve timeout.";
|
||||
case error::connect_timeout: return "Connect timeout.";
|
||||
case error::pong_timeout: return "Pong timeout.";
|
||||
default: BOOST_ASSERT(false); return "Boost.Redis error.";
|
||||
switch (static_cast<error>(ev)) {
|
||||
case error::invalid_data_type: return "Invalid resp3 type.";
|
||||
case error::not_a_number:
|
||||
return "Can't convert string to number (maybe forgot to upgrade to RESP3?).";
|
||||
case error::exceeeds_max_nested_depth:
|
||||
return "Exceeds the maximum number of nested responses.";
|
||||
case error::unexpected_bool_value: return "Unexpected bool value.";
|
||||
case error::empty_field: return "Expected field value is empty.";
|
||||
case error::expects_resp3_simple_type: return "Expects a resp3 simple type.";
|
||||
case error::expects_resp3_aggregate: return "Expects resp3 aggregate.";
|
||||
case error::expects_resp3_map: return "Expects resp3 map.";
|
||||
case error::expects_resp3_set: return "Expects resp3 set.";
|
||||
case error::nested_aggregate_not_supported: return "Nested aggregate not_supported.";
|
||||
case error::resp3_simple_error: return "Got RESP3 simple-error.";
|
||||
case error::resp3_blob_error: return "Got RESP3 blob-error.";
|
||||
case error::incompatible_size: return "Aggregate container has incompatible size.";
|
||||
case error::not_a_double: return "Not a double.";
|
||||
case error::resp3_null: return "Got RESP3 null.";
|
||||
case error::not_connected: return "Not connected.";
|
||||
case error::resolve_timeout: return "Resolve timeout.";
|
||||
case error::connect_timeout: return "Connect timeout.";
|
||||
case error::pong_timeout: return "Pong timeout.";
|
||||
case error::ssl_handshake_timeout: return "SSL handshake timeout.";
|
||||
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::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.";
|
||||
default: BOOST_ASSERT(false); return "Boost.Redis error.";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto category() -> system::error_category const&
|
||||
{
|
||||
static error_category_impl instance;
|
||||
return instance;
|
||||
static error_category_impl instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // detail
|
||||
} // namespace detail
|
||||
|
||||
auto make_error_code(error e) -> system::error_code
|
||||
{
|
||||
return system::error_code{static_cast<int>(e), detail::category()};
|
||||
return system::error_code{static_cast<int>(e), detail::category()};
|
||||
}
|
||||
|
||||
} // boost::redis::detail
|
||||
} // namespace boost::redis
|
||||
|
||||
92
include/boost/redis/impl/exec_fsm.ipp
Normal file
92
include/boost/redis/impl/exec_fsm.ipp
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_EXEC_FSM_IPP
|
||||
#define BOOST_REDIS_EXEC_FSM_IPP
|
||||
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/exec_fsm.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
inline bool is_cancellation(asio::cancellation_type_t type)
|
||||
{
|
||||
return !!(
|
||||
type & (asio::cancellation_type_t::total | asio::cancellation_type_t::partial |
|
||||
asio::cancellation_type_t::terminal));
|
||||
}
|
||||
|
||||
exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Check whether the user wants to wait for the connection to
|
||||
// be established.
|
||||
if (elem_->get_request().get_config().cancel_if_not_connected && !connection_is_open) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, exec_action_type::immediate)
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
return system::error_code(error::not_connected);
|
||||
}
|
||||
|
||||
// No more immediate errors. Set up the supported cancellation types.
|
||||
// This is required to get partial and total cancellations.
|
||||
// This is a potentially allocating operation, so do it as late as we can.
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, exec_action_type::setup_cancellation)
|
||||
|
||||
// Add the request to the multiplexer
|
||||
mpx_->add(elem_);
|
||||
|
||||
// Notify the writer task that there is work to do. If the task is not
|
||||
// listening (e.g. it's already writing or the connection is not healthy),
|
||||
// this is a no-op. Since this is sync, no cancellation can happen here.
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, exec_action_type::notify_writer)
|
||||
|
||||
while (true) {
|
||||
// Wait until we get notified. This will return once the request completes,
|
||||
// or upon any kind of cancellation
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, exec_action_type::wait_for_response)
|
||||
|
||||
// If the request has completed (with error or not), we're done
|
||||
if (elem_->is_done()) {
|
||||
exec_action act{elem_->get_error(), elem_->get_read_size()};
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
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)
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
return exec_action{asio::error::operation_aborted};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We should never get here
|
||||
BOOST_ASSERT(false);
|
||||
return exec_action{system::error_code()};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <boost/redis/ignore.hpp>
|
||||
|
||||
namespace boost::redis
|
||||
{
|
||||
namespace boost::redis {
|
||||
ignore_t ignore;
|
||||
}
|
||||
|
||||
37
include/boost/redis/impl/log_to_file.hpp
Normal file
37
include/boost/redis/impl/log_to_file.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// 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_LOG_TO_STDERR_HPP
|
||||
#define BOOST_REDIS_LOG_TO_STDERR_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Shared by several ipp files
|
||||
inline void log_to_file(FILE* f, std::string_view msg, const char* prefix = "(Boost.Redis) ")
|
||||
{
|
||||
// If the message is empty, data() might return a null pointer
|
||||
const char* msg_ptr = msg.empty() ? "" : msg.data();
|
||||
|
||||
// Precision should be an int when passed to fprintf. Technically,
|
||||
// message could be larger than INT_MAX. Impose a sane limit on message sizes
|
||||
// to prevent memory problems
|
||||
auto precision = static_cast<int>((std::min)(msg.size(), static_cast<std::size_t>(0xffffu)));
|
||||
|
||||
// Log the message. None of our messages should contain NULL bytes, so this should be OK.
|
||||
// We choose fprintf over std::clog because it's safe in multi-threaded environments.
|
||||
std::fprintf(f, "%s%.*s\n", prefix, precision, msg_ptr);
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
@@ -1,128 +1,24 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
//
|
||||
// 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/impl/log_to_file.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
|
||||
namespace boost::redis
|
||||
{
|
||||
#include <cstdio>
|
||||
#include <string_view>
|
||||
|
||||
void logger::write_prefix()
|
||||
{
|
||||
if (!std::empty(prefix_))
|
||||
std::clog << prefix_;
|
||||
}
|
||||
namespace boost::redis {
|
||||
|
||||
void logger::on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
logger::logger(level l)
|
||||
: lvl{l}
|
||||
, fn{[](level, std::string_view msg) {
|
||||
detail::log_to_file(stderr, msg);
|
||||
}}
|
||||
{ }
|
||||
|
||||
write_prefix();
|
||||
|
||||
std::clog << "Resolve results: ";
|
||||
|
||||
if (ec) {
|
||||
std::clog << ec.message() << std::endl;
|
||||
} else {
|
||||
auto begin = std::cbegin(res);
|
||||
auto end = std::cend(res);
|
||||
|
||||
if (begin == end)
|
||||
return;
|
||||
|
||||
std::clog << begin->endpoint();
|
||||
for (auto iter = std::next(begin); iter != end; ++iter)
|
||||
std::clog << ", " << iter->endpoint();
|
||||
}
|
||||
|
||||
std::clog << std::endl;
|
||||
}
|
||||
|
||||
void logger::on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
|
||||
write_prefix();
|
||||
|
||||
std::clog << "Connected to endpoint: ";
|
||||
|
||||
if (ec)
|
||||
std::clog << ec.message() << std::endl;
|
||||
else
|
||||
std::clog << ep;
|
||||
|
||||
std::clog << std::endl;
|
||||
}
|
||||
|
||||
void logger::on_ssl_handshake(system::error_code const& ec)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
|
||||
write_prefix();
|
||||
|
||||
std::clog << "SSL handshake: " << ec.message() << std::endl;
|
||||
}
|
||||
|
||||
void logger::on_connection_lost(system::error_code const& ec)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
|
||||
write_prefix();
|
||||
|
||||
if (ec)
|
||||
std::clog << "Connection lost: " << ec.message();
|
||||
else
|
||||
std::clog << "Connection lost.";
|
||||
|
||||
std::clog << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
logger::on_write(
|
||||
system::error_code const& ec,
|
||||
std::string const& payload)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
|
||||
write_prefix();
|
||||
|
||||
if (ec)
|
||||
std::clog << "Write: " << ec.message();
|
||||
else
|
||||
std::clog << "Bytes written: " << std::size(payload);
|
||||
|
||||
std::clog << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
logger::on_hello(
|
||||
system::error_code const& ec,
|
||||
generic_response const& resp)
|
||||
{
|
||||
if (level_ < level::info)
|
||||
return;
|
||||
|
||||
write_prefix();
|
||||
|
||||
if (ec) {
|
||||
std::clog << "Hello: " << ec.message();
|
||||
if (resp.has_error())
|
||||
std::clog << " (" << resp.error().diagnostic << ")";
|
||||
} else {
|
||||
std::clog << "Hello: Success";
|
||||
}
|
||||
|
||||
std::clog << std::endl;
|
||||
}
|
||||
|
||||
} // boost::redis
|
||||
} // namespace boost::redis
|
||||
|
||||
322
include/boost/redis/impl/multiplexer.ipp
Normal file
322
include/boost/redis/impl/multiplexer.ipp
Normal file
@@ -0,0 +1,322 @@
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
multiplexer::elem::elem(request const& req, any_adapter adapter)
|
||||
: req_{&req}
|
||||
, adapter_{std::move(adapter)}
|
||||
, remaining_responses_{req.get_expected_responses()}
|
||||
, status_{status::waiting}
|
||||
, ec_{}
|
||||
, read_size_{0}
|
||||
{ }
|
||||
|
||||
auto multiplexer::elem::notify_error(system::error_code ec) noexcept -> void
|
||||
{
|
||||
if (!ec_) {
|
||||
ec_ = ec;
|
||||
}
|
||||
|
||||
notify_done();
|
||||
}
|
||||
|
||||
auto multiplexer::elem::commit_response(std::size_t read_size) -> void
|
||||
{
|
||||
read_size_ += read_size;
|
||||
--remaining_responses_;
|
||||
}
|
||||
|
||||
bool multiplexer::remove(std::shared_ptr<elem> const& ptr)
|
||||
{
|
||||
if (ptr->is_waiting()) {
|
||||
reqs_.erase(std::remove(std::begin(reqs_), std::end(reqs_), ptr));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t multiplexer::commit_write()
|
||||
{
|
||||
// We have to clear the payload right after writing it to use it
|
||||
// as a flag that informs there is no ongoing write.
|
||||
write_buffer_.clear();
|
||||
|
||||
// There is small optimization possible here: traverse only the
|
||||
// partition of unwritten requests instead of them all.
|
||||
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
|
||||
BOOST_ASSERT_MSG(ptr != nullptr, "Expects non-null pointer.");
|
||||
if (ptr->is_staged()) {
|
||||
ptr->mark_written();
|
||||
}
|
||||
});
|
||||
|
||||
return release_push_requests();
|
||||
}
|
||||
|
||||
void multiplexer::add(std::shared_ptr<elem> const& info)
|
||||
{
|
||||
reqs_.push_back(info);
|
||||
|
||||
if (info->get_request().has_hello_priority()) {
|
||||
auto rend = std::partition_point(std::rbegin(reqs_), std::rend(reqs_), [](auto const& e) {
|
||||
return e->is_waiting();
|
||||
});
|
||||
|
||||
std::rotate(std::rbegin(reqs_), std::rbegin(reqs_) + 1, rend);
|
||||
}
|
||||
}
|
||||
|
||||
tribool multiplexer::consume_next_impl(std::string_view data, system::error_code& ec)
|
||||
{
|
||||
// We arrive here in two states:
|
||||
//
|
||||
// 1. While we are parsing a message. In this case we
|
||||
// don't want to determine the type of the message in the
|
||||
// buffer (i.e. response vs push) but leave it untouched
|
||||
// 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.
|
||||
//
|
||||
|
||||
BOOST_ASSERT(!data.empty());
|
||||
if (!on_push_) // Prepare for new message.
|
||||
on_push_ = is_next_push(data);
|
||||
|
||||
if (on_push_) {
|
||||
if (!resp3::parse(parser_, data, receive_adapter_, ec))
|
||||
return std::nullopt;
|
||||
|
||||
return std::make_optional(true);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!resp3::parse(parser_, data, reqs_.front()->get_adapter(), ec))
|
||||
return std::nullopt;
|
||||
|
||||
if (ec) {
|
||||
reqs_.front()->notify_error(ec);
|
||||
reqs_.pop_front();
|
||||
return std::make_optional(false);
|
||||
}
|
||||
|
||||
reqs_.front()->commit_response(parser_.get_consumed());
|
||||
if (reqs_.front()->get_remaining_responses() == 0) {
|
||||
// Done with this request.
|
||||
reqs_.front()->notify_done();
|
||||
reqs_.pop_front();
|
||||
}
|
||||
|
||||
return std::make_optional(false);
|
||||
}
|
||||
|
||||
std::pair<tribool, std::size_t> multiplexer::consume_next(
|
||||
std::string_view data,
|
||||
system::error_code& ec)
|
||||
{
|
||||
auto const ret = consume_next_impl(data, ec);
|
||||
auto const consumed = parser_.get_consumed();
|
||||
if (ec) {
|
||||
return std::make_pair(ret, consumed);
|
||||
}
|
||||
|
||||
if (ret.has_value()) {
|
||||
parser_.reset();
|
||||
commit_usage(ret.value(), consumed);
|
||||
return std::make_pair(ret, consumed);
|
||||
}
|
||||
|
||||
return std::make_pair(std::nullopt, consumed);
|
||||
}
|
||||
|
||||
void multiplexer::reset()
|
||||
{
|
||||
write_buffer_.clear();
|
||||
parser_.reset();
|
||||
on_push_ = false;
|
||||
cancel_run_called_ = false;
|
||||
}
|
||||
|
||||
std::size_t multiplexer::prepare_write()
|
||||
{
|
||||
// Coalesces the requests and marks them staged. After a
|
||||
// successful write staged requests will be marked as written.
|
||||
auto const point = std::partition_point(
|
||||
std::cbegin(reqs_),
|
||||
std::cend(reqs_),
|
||||
[](auto const& ri) {
|
||||
return !ri->is_waiting();
|
||||
});
|
||||
|
||||
std::for_each(point, std::cend(reqs_), [this](auto const& ri) {
|
||||
// Stage the request.
|
||||
write_buffer_ += ri->get_request().payload();
|
||||
ri->mark_staged();
|
||||
usage_.commands_sent += ri->get_request().get_commands();
|
||||
});
|
||||
|
||||
usage_.bytes_sent += std::size(write_buffer_);
|
||||
|
||||
auto const d = std::distance(point, std::cend(reqs_));
|
||||
return static_cast<std::size_t>(d);
|
||||
}
|
||||
|
||||
std::size_t multiplexer::cancel_waiting()
|
||||
{
|
||||
auto f = [](auto const& ptr) {
|
||||
BOOST_ASSERT(ptr != nullptr);
|
||||
return !ptr->is_waiting();
|
||||
};
|
||||
|
||||
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), f);
|
||||
|
||||
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});
|
||||
});
|
||||
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto multiplexer::cancel_on_conn_lost() -> std::size_t
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Must return false if the request should be removed.
|
||||
auto cond = [](auto const& ptr) {
|
||||
BOOST_ASSERT(ptr != nullptr);
|
||||
|
||||
if (ptr->is_waiting()) {
|
||||
return !ptr->get_request().get_config().cancel_on_connection_lost;
|
||||
} else {
|
||||
return !ptr->get_request().get_config().cancel_if_unresponded;
|
||||
}
|
||||
};
|
||||
|
||||
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});
|
||||
});
|
||||
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
|
||||
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
|
||||
return ptr->mark_waiting();
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void multiplexer::commit_usage(bool is_push, std::size_t size)
|
||||
{
|
||||
if (is_push) {
|
||||
usage_.pushes_received += 1;
|
||||
usage_.push_bytes_received += size;
|
||||
on_push_ = false;
|
||||
} else {
|
||||
usage_.responses_received += 1;
|
||||
usage_.response_bytes_received += size;
|
||||
}
|
||||
}
|
||||
|
||||
bool multiplexer::is_next_push(std::string_view data) const noexcept
|
||||
{
|
||||
// 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
|
||||
|
||||
// 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
|
||||
// noticed this is possible, for example with -MISCONF. I don't
|
||||
// know why they are not sent with a push type so we can
|
||||
// distinguish them from responses to commands. If we are lucky
|
||||
// enough to receive them when the command queue is empty they
|
||||
// can be treated as server pushes, otherwise it is impossible
|
||||
// to handle them properly
|
||||
if (reqs_.empty())
|
||||
return true;
|
||||
|
||||
// The request does not expect any response but we got one. This
|
||||
// may happen if for example, subscribe with wrong syntax.
|
||||
if (reqs_.front()->get_remaining_responses() == 0)
|
||||
return true;
|
||||
|
||||
// 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
|
||||
// written.
|
||||
return reqs_.front()->is_waiting();
|
||||
}
|
||||
|
||||
std::size_t 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);
|
||||
});
|
||||
|
||||
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
|
||||
ptr->notify_done();
|
||||
});
|
||||
|
||||
auto const d = std::distance(point, std::end(reqs_));
|
||||
reqs_.erase(point, std::end(reqs_));
|
||||
return static_cast<std::size_t>(d);
|
||||
}
|
||||
|
||||
bool multiplexer::is_waiting_response() const noexcept
|
||||
{
|
||||
if (std::empty(reqs_))
|
||||
return false;
|
||||
|
||||
// Under load and on low-latency networks we might start
|
||||
// receiving responses before the write operation completed and
|
||||
// the request is still maked as staged and not written. See
|
||||
// https://github.com/boostorg/redis/issues/170
|
||||
return !reqs_.front()->is_waiting();
|
||||
}
|
||||
|
||||
bool multiplexer::is_writing() const noexcept { return !write_buffer_.empty(); }
|
||||
|
||||
void multiplexer::set_receive_adapter(any_adapter adapter)
|
||||
{
|
||||
receive_adapter_ = std::move(adapter);
|
||||
}
|
||||
|
||||
auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>
|
||||
{
|
||||
return std::make_shared<multiplexer::elem>(req, std::move(adapter));
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
79
include/boost/redis/impl/read_buffer.ipp
Normal file
79
include/boost/redis/impl/read_buffer.ipp
Normal file
@@ -0,0 +1,79 @@
|
||||
/* 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_append()
|
||||
{
|
||||
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_append(std::size_t read_size)
|
||||
{
|
||||
BOOST_ASSERT(buffer_.size() >= (append_buf_begin_ + read_size));
|
||||
buffer_.resize(append_buf_begin_ + read_size);
|
||||
append_buf_begin_ = buffer_.size();
|
||||
}
|
||||
|
||||
auto read_buffer::get_append_buffer() noexcept -> span_type
|
||||
{
|
||||
auto const size = buffer_.size();
|
||||
return make_span(buffer_.data() + append_buf_begin_, size - append_buf_begin_);
|
||||
}
|
||||
|
||||
auto read_buffer::get_committed_buffer() const noexcept -> std::string_view
|
||||
{
|
||||
BOOST_ASSERT(!buffer_.empty());
|
||||
return {buffer_.data(), append_buf_begin_};
|
||||
}
|
||||
|
||||
auto read_buffer::get_committed_size() const noexcept -> std::size_t { return append_buf_begin_; }
|
||||
|
||||
void read_buffer::clear()
|
||||
{
|
||||
buffer_.clear();
|
||||
append_buf_begin_ = 0;
|
||||
}
|
||||
|
||||
std::size_t read_buffer::consume_committed(std::size_t size)
|
||||
{
|
||||
// For convenience, if the requested size is larger than the
|
||||
// committed buffer we cap it to the maximum.
|
||||
if (size > append_buf_begin_)
|
||||
size = append_buf_begin_;
|
||||
|
||||
buffer_.erase(buffer_.begin(), buffer_.begin() + size);
|
||||
BOOST_ASSERT(append_buf_begin_ >= size);
|
||||
append_buf_begin_ -= size;
|
||||
return size;
|
||||
}
|
||||
|
||||
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
|
||||
88
include/boost/redis/impl/reader_fsm.ipp
Normal file
88
include/boost/redis/impl/reader_fsm.ipp
Normal file
@@ -0,0 +1,88 @@
|
||||
/* 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/coroutine.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/detail/reader_fsm.hpp>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
reader_fsm::reader_fsm(read_buffer& rbuf, multiplexer& mpx) noexcept
|
||||
: read_buffer_{&rbuf}
|
||||
, mpx_{&mpx}
|
||||
{ }
|
||||
|
||||
reader_fsm::action reader_fsm::resume(
|
||||
std::size_t bytes_read,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t /*cancel_state*/)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, action::type::setup_cancellation)
|
||||
|
||||
for (;;) {
|
||||
ec = read_buffer_->prepare_append();
|
||||
if (ec) {
|
||||
action_after_resume_ = {action::type::done, 0, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
}
|
||||
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, next_read_type_)
|
||||
read_buffer_->commit_append(bytes_read);
|
||||
if (ec) {
|
||||
// TODO: If an error occurred but data was read (i.e.
|
||||
// bytes_read != 0) we should try to process that data and
|
||||
// deliver it to the user before calling cancel_run.
|
||||
action_after_resume_ = {action::type::done, bytes_read, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
}
|
||||
|
||||
next_read_type_ = action::type::append_some;
|
||||
while (read_buffer_->get_committed_size() != 0) {
|
||||
res_ = mpx_->consume_next(read_buffer_->get_committed_buffer(), ec);
|
||||
if (ec) {
|
||||
// TODO: Perhaps log what has not been consumed to aid
|
||||
// debugging.
|
||||
action_after_resume_ = {action::type::done, res_.second, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 5, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
}
|
||||
|
||||
if (!res_.first.has_value()) {
|
||||
next_read_type_ = action::type::needs_more;
|
||||
break;
|
||||
}
|
||||
|
||||
read_buffer_->consume_committed(res_.second);
|
||||
|
||||
if (res_.first.value()) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 6, action::type::notify_push_receiver, res_.second)
|
||||
if (ec) {
|
||||
action_after_resume_ = {action::type::done, 0u, ec};
|
||||
BOOST_REDIS_YIELD(resume_point_, 7, action::type::cancel_run)
|
||||
return action_after_resume_;
|
||||
}
|
||||
} else {
|
||||
// TODO: Here we should notify the exec operation that
|
||||
// it can be completed. This will improve log clarity
|
||||
// and will make this code symmetrical in how it
|
||||
// handles pushes and other messages. The new action
|
||||
// type can be named notify_exec. To do that we need to
|
||||
// refactor the multiplexer.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASSERT(false);
|
||||
return {action::type::done, 0, system::error_code()};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -12,10 +12,13 @@ namespace boost::redis::detail {
|
||||
|
||||
auto has_response(std::string_view cmd) -> bool
|
||||
{
|
||||
if (cmd == "SUBSCRIBE") return true;
|
||||
if (cmd == "PSUBSCRIBE") return true;
|
||||
if (cmd == "UNSUBSCRIBE") return true;
|
||||
if (cmd == "SUBSCRIBE")
|
||||
return true;
|
||||
if (cmd == "PSUBSCRIBE")
|
||||
return true;
|
||||
if (cmd == "UNSUBSCRIBE")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // boost:redis::detail
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
26
include/boost/redis/impl/resp3_handshaker.ipp
Normal file
26
include/boost/redis/impl/resp3_handshaker.ipp
Normal file
@@ -0,0 +1,26 @@
|
||||
/* 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
|
||||
49
include/boost/redis/impl/response.ipp
Normal file
49
include/boost/redis/impl/response.ipp
Normal file
@@ -0,0 +1,49 @@
|
||||
/* 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/error.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
void consume_one(generic_response& r, system::error_code& ec)
|
||||
{
|
||||
if (r.has_error())
|
||||
return; // Nothing to consume.
|
||||
|
||||
if (std::empty(r.value()))
|
||||
return; // Nothing to consume.
|
||||
|
||||
auto const depth = r.value().front().depth;
|
||||
|
||||
// To simplify we will refuse to consume any data-type that is not
|
||||
// a root node. I think there is no use for that and it is complex
|
||||
// since it requires updating parent nodes.
|
||||
if (depth != 0) {
|
||||
ec = error::incompatible_node_depth;
|
||||
return;
|
||||
}
|
||||
|
||||
auto f = [depth](auto const& e) {
|
||||
return e.depth == depth;
|
||||
};
|
||||
|
||||
auto match = std::find_if(std::next(std::cbegin(r.value())), std::cend(r.value()), f);
|
||||
|
||||
r.value().erase(std::cbegin(r.value()), match);
|
||||
}
|
||||
|
||||
void consume_one(generic_response& r)
|
||||
{
|
||||
system::error_code ec;
|
||||
consume_one(r, ec);
|
||||
if (ec)
|
||||
throw system::system_error(ec);
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -7,26 +7,23 @@
|
||||
#ifndef BOOST_REDIS_LOGGER_HPP
|
||||
#define BOOST_REDIS_LOGGER_HPP
|
||||
|
||||
#include <boost/redis/response.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace boost::system {class error_code;}
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
/** @brief Logger class
|
||||
* @ingroup high-level-api
|
||||
/** @brief Defines logging configuration.
|
||||
*
|
||||
* The class can be passed to the connection objects to log to `std::clog`
|
||||
* See the member descriptions for more info.
|
||||
*/
|
||||
class logger {
|
||||
public:
|
||||
/** @brief Syslog-like log levels
|
||||
* @ingroup high-level-api
|
||||
*/
|
||||
struct logger {
|
||||
/// Syslog-like log levels.
|
||||
enum class level
|
||||
{ /// Emergency
|
||||
{
|
||||
/// Disabled
|
||||
disabled,
|
||||
|
||||
/// Emergency
|
||||
emerg,
|
||||
|
||||
/// Alert
|
||||
@@ -48,80 +45,53 @@ public:
|
||||
info,
|
||||
|
||||
/// Debug
|
||||
debug
|
||||
debug,
|
||||
};
|
||||
|
||||
/** @brief Constructor
|
||||
* @ingroup high-level-api
|
||||
/** @brief Constructor from a level.
|
||||
*
|
||||
* @param l Log level.
|
||||
*/
|
||||
logger(level l = level::info)
|
||||
: level_{l}
|
||||
{}
|
||||
|
||||
/** @brief Called when the resolve operation completes.
|
||||
* @ingroup high-level-api
|
||||
* Constructs a logger with the specified level
|
||||
* and a logging function that prints messages to `stderr`.
|
||||
*
|
||||
* @param ec Error returned by the resolve operation.
|
||||
* @param res Resolve results.
|
||||
*/
|
||||
void on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res);
|
||||
|
||||
/** @brief Called when the connect operation completes.
|
||||
* @ingroup high-level-api
|
||||
* @param l The value to set @ref lvl to.
|
||||
*
|
||||
* @param ec Error returned by the connect operation.
|
||||
* @param ep Endpoint to which the connection connected.
|
||||
* @par Exceptions
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep);
|
||||
logger(level l = level::info);
|
||||
|
||||
/** @brief Called when the ssl handshake operation completes.
|
||||
* @ingroup high-level-api
|
||||
/** @brief Constructor from a level and a function.
|
||||
*
|
||||
* @param ec Error returned by the handshake operation.
|
||||
*/
|
||||
void on_ssl_handshake(system::error_code const& ec);
|
||||
|
||||
/** @brief Called when the connection is lost.
|
||||
* @ingroup high-level-api
|
||||
* Constructs a logger by setting its members to the specified values.
|
||||
*
|
||||
* @param ec Error returned when the connection is lost.
|
||||
*/
|
||||
void on_connection_lost(system::error_code const& ec);
|
||||
|
||||
/** @brief Called when the write operation completes.
|
||||
* @ingroup high-level-api
|
||||
* @param l The value to set @ref lvl to.
|
||||
* @param fn The value to set @ref fn to.
|
||||
*
|
||||
* @param ec Error code returned by the write operation.
|
||||
* @param payload The payload written to the socket.
|
||||
* @par Exceptions
|
||||
* No-throw guarantee.
|
||||
*/
|
||||
void on_write(system::error_code const& ec, std::string const& payload);
|
||||
logger(level l, std::function<void(level, std::string_view)> fn)
|
||||
: lvl{l}
|
||||
, fn{std::move(fn)}
|
||||
{ }
|
||||
|
||||
/** @brief Called when the `HELLO` request completes.
|
||||
* @ingroup high-level-api
|
||||
/**
|
||||
* @brief Defines a severity filter for messages.
|
||||
*
|
||||
* @param ec Error code returned by the async_exec operation.
|
||||
* @param resp Response sent by the Redis server.
|
||||
* Only messages with a level >= to the one specified by the logger
|
||||
* will be logged.
|
||||
*/
|
||||
void on_hello(system::error_code const& ec, generic_response const& resp);
|
||||
level lvl;
|
||||
|
||||
/** @brief Sets a prefix to every log message
|
||||
* @ingroup high-level-api
|
||||
/**
|
||||
* @brief Defines a severity filter for messages.
|
||||
*
|
||||
* @param prefix The prefix.
|
||||
* Only messages with a level >= to the one specified by the logger
|
||||
* will be logged.
|
||||
*/
|
||||
void set_prefix(std::string_view prefix)
|
||||
{
|
||||
prefix_ = prefix;
|
||||
}
|
||||
|
||||
private:
|
||||
void write_prefix();
|
||||
level level_;
|
||||
std::string_view prefix_;
|
||||
std::function<void(level, std::string_view)> fn;
|
||||
};
|
||||
|
||||
} // boost::redis
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif // BOOST_REDIS_LOGGER_HPP
|
||||
#endif // BOOST_REDIS_LOGGER_HPP
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user