2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-24 06:22:07 +00:00

Compare commits

...

81 Commits

Author SHA1 Message Date
Marcelo
84ee2f37f1 Merge pull request #141 from boostorg/develop
Pre-Boost release of latest changes in develop
2023-08-18 23:08:05 +02:00
Marcelo
81927deda4 Merge pull request #134 from boostorg/123-automatically-select-database-after-hello
123 automatically select database after hello
2023-08-06 11:00:34 +02:00
Marcelo Zimbres
34ff1cea63 Fixes https://github.com/boostorg/redis/issues/121 2023-08-06 10:07:17 +02:00
Marcelo Zimbres
10603b7d3a Sends SELECT right after HELLO after a connection. 2023-08-06 10:07:17 +02:00
Marcelo
ad3c2914db Merge pull request #133 from boostorg/122-health-check-not-restart-after-connection-lost
Simplifies parse ops and fixes health-check on reconnection.
2023-08-05 23:04:50 +02:00
Marcelo Zimbres
91014b13bf Simplifies parse ops and fixes health-check on reconnection. 2023-08-05 22:20:39 +02:00
Marcelo
4f6f8b454d Merge pull request #132 from cbodley/wip-exec-forward
connection: async_exec forwards completion token
2023-08-02 22:06:27 +02:00
Casey Bodley
9ebcc544ae connection: async_exec forwards completion token
async operations should support move-only completion handlers. forward
the CompletionToken argument to avoid an unnecessary copy

Fixes: #131

Signed-off-by: Casey Bodley <cbodley@redhat.com>
2023-08-02 11:45:57 -04:00
Marcelo
7d16259749 Merge pull request #130 from boostorg/119-simplify-the-parser-op
Simplifications in the parser
2023-07-30 11:44:58 +02:00
Marcelo Zimbres
9dec63515e Simplifications in the parser. 2023-07-30 08:18:17 +02:00
Marcelo
46525371b9 Merge pull request #117 from cthulhu-irl/build/add-cmake-options
build: add cmake options
2023-07-01 13:03:31 +02:00
Cthulhu
b5f8348598 build: add cmake options
cmake options for install, tests, examples, and doc instead of
building them always.

options are enabled by default when building the project directly,
otherwise if add_subdirectory (directly or by FetchContent, etc),
then unnecessary options will be disabled.

issue #115
2023-06-25 19:26:27 +03:30
Marcelo
69d12421e2 Merge pull request #116 from boostorg/113-create-an-experimental-connection-class-that-has-fast-compilation-times
113 create an experimental connection class that has fast compilation times
2023-06-24 09:52:48 +02:00
Marcelo Zimbres
a715c251bf Improvements in the docs. 2023-06-23 22:24:34 +02:00
Marcelo Zimbres
d29a057fa6 Uses composition instead of inheritance in the connection class. 2023-06-20 23:01:17 +02:00
Marcelo Zimbres
82430afc8b Make the connection non-generic on the executor type. 2023-06-18 09:34:40 +02:00
Marcelo
607946f00e Merge pull request #112 from boostorg/111-simplify-the-serialization-examples
111 simplify the serialization examples
2023-06-11 11:12:06 +02:00
Marcelo Zimbres
c99790ab5c Uses choco instead of cinst. 2023-06-11 09:28:12 +02:00
Marcelo Zimbres
635b3608ad Removes unnecessary files. 2023-06-08 22:00:07 +02:00
Marcelo
a8a78c38c6 Merge pull request #109 from boostorg/103-cant-read-response-of-hello-+-ping-+-hello
Removes payload rotation from request.
2023-06-04 16:50:12 +02:00
Marcelo Zimbres
e09a53ff08 Removes payload rotation from request.
The user can simply call HELLO before other commands. Altering the order
of requests makes it impossible to declare responses.
2023-06-04 16:18:35 +02:00
Marcelo
ec8a1c7286 Merge pull request #105 from boostorg/95-improve-the-performance-of-connectionasync_receive
95 improve the performance of connectionasync receive
2023-05-28 11:14:14 +02:00
Marcelo Zimbres
3c02a7662b Replaces connection channel with a timer. 2023-05-27 23:20:15 +02:00
Marcelo Zimbres
538ab8f35f Reduces the number of rescheduling needed to process a server sent push.
Performance improved by close to 10%.
2023-05-21 21:17:13 +02:00
Marcelo Zimbres
f5f57e370b Improvements in the redis-push stress test. 2023-05-21 21:17:13 +02:00
Marcelo
7abfc5fd8d Merge pull request #101 from boostorg/100-runhpp-no-longer-present-but-still-referred-to-from-redishpp
Fixes redis.hpp and slightly improves compilation times.
2023-05-20 14:16:19 +02:00
Marcelo Zimbres
11eebcf771 Fixes redis.hpp and slightly improves compilation times. 2023-05-20 13:19:31 +02:00
Marcelo
c21f70bc07 Merge pull request #99 from boostorg/93-use-cmake-foreach-to-simplify-cmakeliststxt
Simplifies the CMakeLists.txt.
2023-05-14 13:55:46 +02:00
Marcelo Zimbres
22bacbd52c Simplifies the CMakeLists.txt. 2023-05-14 10:42:16 +02:00
Marcelo
2982f831f6 Merge pull request #98 from boostorg/94-unify-redisconnection-and-redissslconnection
94 unify redisconnection and redissslconnection
2023-05-13 18:22:53 +02:00
Marcelo Zimbres
663e9ac671 Simplifications. 2023-05-13 10:22:11 +02:00
Marcelo Zimbres
c0aa4356ea The ssl::context is now owned by the connection. 2023-05-10 23:25:09 +02:00
Marcelo Zimbres
6f9fd5b2fb Unifies ssl and plain connections. 2023-05-09 23:12:16 +02:00
Marcelo
30a6e34e4e Merge pull request #97 from boostorg/85-added-example-cpp20_streams-which-reproduces-an-assertion
85 added example cpp20 streams which reproduces an assertion
2023-05-06 21:26:09 +02:00
Marcelo Zimbres
1f9b3e8008 Rebase the branch on develop. 2023-05-06 20:48:32 +02:00
bram
3808fec0e3 Cleaned up a bit
Removed unused stuff
Using request and response as shared_ptrs.
Removed (unnecessary?) calls to net::post.
2023-05-06 17:14:10 +02:00
bram
607a9e9dd6 Added example cpp20_streams, which reproduces an assertion. 2023-05-06 16:55:15 +02:00
Marcelo
2d53bb748e Merge pull request #92 from boostorg/90-add-support-for-reconnection
90 add support for reconnection
2023-05-06 15:39:29 +02:00
Marcelo Zimbres
a6cb4ca323 Adds high-level functionality to connection::async_run. 2023-05-02 23:15:08 +02:00
Marcelo Zimbres
5ac4f7e8ad Removes dependency on asio::promise as it does not compile on windows. 2023-03-31 04:00:20 +02:00
Marcelo Zimbres
7a08588808 Progresses with the subscriber. 2023-03-31 04:00:19 +02:00
Marcelo
e7ff1cedf3 Merge pull request #89 from boostorg/88-simplify-async_check_health-with-asioconsign
Uses consign to simplify the check-health operation.
2023-03-20 01:53:40 +01:00
Marcelo Zimbres
0bcb7dcf16 Uses consign to simplify the check-health operation. 2023-03-20 01:26:50 +01:00
Marcelo
c28969674b Merge pull request #86 from boostorg/83-fix-reconnect-loop-in-the-subscriber-example
83 fix reconnect loop in the subscriber example
2023-03-16 18:05:40 +01:00
Marcelo Zimbres
c7f49c6677 Adds address struct. 2023-03-16 17:01:49 +01:00
Marcelo Zimbres
90bcd621fb Including only necessary headers. 2023-03-14 20:11:22 +01:00
Marcelo Zimbres
fd967204df Implements non-member async_run for plain connections.
This function will resolve and connect before calling member async_run.
2023-03-13 21:37:25 +01:00
Marcelo
cd00047a49 Merge pull request #82 from boostorg/81-add-support-for-protobuf
First steps with protobuf support.
2023-03-11 13:10:39 +01:00
Marcelo Zimbres
728b35cfe0 Adds protobuf example. 2023-03-11 12:24:35 +01:00
Marcelo
52e62ba78c Merge pull request #80 from boostorg/79-missing-header-in-resulthpp
Adds missing include header.
2023-03-08 12:10:37 +01:00
Marcelo Zimbres
bb18ff4891 Adds missing include header. 2023-03-07 20:24:53 +01:00
Marcelo
6ce793e413 Merge pull request #78 from boostorg/69-move-health-check-functionality-from-examples-to-the-library
Implements a function that checks Redis health.
2023-03-05 21:18:12 +01:00
Marcelo Zimbres
a83c0e7803 Trying to fix build on MSVC by including tuple before asio. 2023-03-05 20:54:02 +01:00
Marcelo Zimbres
64820bd25b Implements a function that checks Redis health. 2023-03-04 15:54:23 +01:00
Marcelo
16b5c8d1ba Merge pull request #77 from boostorg/72-cannot-build-on-macos
Adds support for libc++.
2023-03-02 08:12:36 +01:00
Marcelo Zimbres
8ef4d3cf0b Adds support for libc++. 2023-03-02 07:46:21 +01:00
Marcelo
d01a9acf3b Merge pull request #75 from boostorg/51-more-suggestions-for-documentation-improvements
Addresses issue #45, #47, #51 and #74.
2023-02-26 20:58:14 +01:00
Marcelo Zimbres
ac7e425d47 Addresses issue #45, #47, #51 and #74. 2023-02-26 20:34:43 +01:00
Marcelo
d620cdee59 Merge pull request #71 from boostorg/70-rename-node-to-basic_node-and-add-typedef-for-nodestdstring
70 rename node to basic node and add typedef for nodestdstring
2023-02-25 15:30:04 +01:00
Marcelo Zimbres
5f07b730f7 Upgrades to Boost 1.81. 2023-02-25 10:26:07 +01:00
Marcelo Zimbres
6d3a112f94 Removes json serialization boilerplate. 2023-02-25 10:14:28 +01:00
Marcelo Zimbres
1f3ef6b486 Renames node to basic_node. 2023-02-25 10:14:28 +01:00
Marcelo
a850a6ed63 Merge pull request #68 from boostorg/67-make-the-connection-full-duplex
67 make the connection full duplex
2023-02-18 21:39:13 +01:00
Marcelo Zimbres
c8b73c2fe8 Removes coalesce property of the requests.
It doesn't make any sense after the implementation of full-duplex
communication.
2023-02-18 20:08:55 +01:00
Marcelo Zimbres
8b02268182 Uses boost.describe to simplify json serialization. 2023-02-16 21:44:45 +01:00
Marcelo Zimbres
1b60eeb352 Makes the connection full-duplex. 2023-02-12 19:14:07 +01:00
Marcelo
b93f36163d Merge pull request #66 from boostorg/65-move-redisresp3async_read-to-redis-namespace
Moves read functions from resp3:: to redis::.
2023-02-12 07:44:33 +01:00
Marcelo Zimbres
071f9a93aa Moves read functions from resp3:: to redis::. 2023-02-11 19:17:05 +01:00
Marcelo
5a6ca14a67 Merge pull request #64 from boostorg/40-improve-support-to-redis-error-messages
Uses system::result to implement per request error handling.
2023-02-11 14:56:12 +01:00
Marcelo Zimbres
a5c86107f8 Uses system::result to implement per request error handling. 2023-02-11 11:53:44 +01:00
Marcelo
3a4445022e Merge pull request #63 from boostorg/62-let-the-implementation-call-adaptresp-automatically
async_exec accepts response now instead of adapter.
2023-02-05 10:08:05 +01:00
Marcelo Zimbres
bfb26f2602 async_exec accepts response now instead of adapter. 2023-02-04 20:43:36 +01:00
Marcelo
7e70cb4ad7 Merge pull request #61 from boostorg/60-we-need-response-typedefs
Adds response typedefs.
2023-02-04 10:29:22 +01:00
Marcelo Zimbres
886561409a Adds response typedefs. 2023-02-04 10:03:08 +01:00
Marcelo
0c5ff09685 Merge pull request #59 from boostorg/42-the-names-from_bulk-and-to_bulk-are-too-generic-for-adl-customization-points
Prefix to_ and from_bulk with boost_redis_ (boost review).
2023-02-02 08:33:44 +01:00
Marcelo Zimbres
4b07b6d516 Prefix to_ and from_bulk with boost_redis_ (boost review). 2023-02-01 22:48:33 +01:00
Marcelo
c1ce8358c7 Merge pull request #57 from boostorg/56-remove-memory_resource-usage
Removes memory_resource.
2023-01-29 21:14:37 +01:00
Marcelo Zimbres
13e16b7a60 Removes memory_resource. 2023-01-29 20:21:30 +01:00
Marcelo
e11502e0df Merge pull request #55 from boostorg/54-rename-aedis-to-boost-redis
Renames Aedis to Boost.Redis.
2023-01-28 21:43:57 +01:00
Marcelo Zimbres
b2344384cf Renames Aedis to Boost.Redis. 2023-01-28 17:57:35 +01:00
Marcelo Zimbres
56c0b28003 Fixes issue 50 and 44. 2023-01-28 09:35:36 +01:00
112 changed files with 8517 additions and 6188 deletions

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
boost_version: ["1.80.0"]
boost_version: ["1.81.0"]
os: [windows-2019, windows-2022]
toolset: [v142, v143]
build_type: [Release]
@@ -25,7 +25,7 @@ jobs:
- { 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.80.0", toolset: v143 }
- { boost_version: "1.81.0", toolset: v143 }
steps:
- name: Checkout
@@ -34,7 +34,7 @@ jobs:
fetch-depth: 0
- name: Add boost toolset to environment
if: contains(fromJson('["1.80.0"]'), matrix.boost_version)
if: contains(fromJson('["1.81.0"]'), matrix.boost_version)
run: echo BOOST_TOOLSET=$(echo "msvc") >> $GITHUB_ENV
# The platform_version passed to boost-install determines the msvc toolset version for which static libs are installed.
@@ -67,8 +67,8 @@ jobs:
platform_version: ${{env.BOOST_PLATFORM_VERSION}}
arch: null
- name: Install packages
run: cinst openssl
- name: Install openssl
run: choco install openssl
- name: Create build directory
run: mkdir build
@@ -98,33 +98,38 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxstd: 'c++20' }
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxstd: 'c++20' }
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxstd: 'c++20' }
- { 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 }}
env:
CXXFLAGS: -g -O0 -std=${{matrix.cxxstd}} -Wall -Wextra
CXXFLAGS: -g -O0 ${{matrix.cxxflags}} -Wall -Wextra
LDFLAGS: ${{matrix.ldflags}}
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.3.0
uses: MarkusJx/install-boost@v2.4.1
id: install-boost
with:
boost_version: 1.80.0
boost_version: 1.81.0
platform_version: 22.04
- name: Run CMake
run: |
BOOST_ROOT=${{steps.install-boost.outputs.BOOST_ROOT}} cmake -DCMAKE_CXX_COMPILER="${{matrix.compiler}}" -DCMAKE_CXX_FLAGS="${{env.CXXFLAGS}}"
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

View File

@@ -3,7 +3,7 @@ name: Coverage
on:
push:
branches:
- master
- develop
jobs:
posix:
defaults:
@@ -27,10 +27,10 @@ jobs:
- name: Install Redis
run: sudo apt-get -y install redis-server
- name: Install boost
uses: MarkusJx/install-boost@v2.3.0
uses: MarkusJx/install-boost@v2.4.1
id: install-boost
with:
boost_version: 1.80.0
boost_version: 1.81.0
platform_version: 22.04
- name: Run CMake
run: |

View File

@@ -1,29 +1,39 @@
# At the moment the official build system is still autotools and this
# file is meant to support Aedis on windows.
# BOOST_ROOT=/opt/boost_1_79/ cmake -DCMAKE_CXX_FLAGS="-g -O0
# -std=c++20 -Wall -Wextra --coverage -fkeep-inline-functions
# -fkeep-static-functions" -DCMAKE_EXE_LINKER_FLAGS="--coverage"
# ~/my/aedis
cmake_minimum_required(VERSION 3.14)
#set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
# 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()
endif()
project(
Aedis
boost_redis
VERSION 1.4.1
DESCRIPTION "A redis client library"
HOMEPAGE_URL "https://mzimbres.github.io/aedis"
HOMEPAGE_URL "https://boostorg.github.io/redis/"
LANGUAGES CXX
)
add_library(aedis INTERFACE)
target_include_directories(aedis INTERFACE
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})
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(
aedis
boost_redis
INTERFACE
Boost::asio
Boost::assert
@@ -34,307 +44,187 @@ target_link_libraries(
Boost::utility
)
target_compile_features(aedis INTERFACE cxx_std_17)
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(aedis INTERFACE /Zc:__cplusplus)
target_compile_options(boost_redis INTERFACE /Zc:__cplusplus)
endif()
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${PROJECT_BINARY_DIR}/AedisConfigVersion.cmake"
COMPATIBILITY AnyNewerVersion
)
find_package(Boost 1.80 REQUIRED)
include_directories(${Boost_INCLUDE_DIRS})
find_package(OpenSSL REQUIRED)
enable_testing()
include_directories(include)
# Main function for the examples.
# Common
#=======================================================================
add_library(common STATIC
examples/common/common.cpp
examples/common/main.cpp
examples/common/aedis.cpp
)
target_compile_features(common PUBLIC cxx_std_20)
add_library(boost_redis_project_options INTERFACE)
target_link_libraries(boost_redis_project_options INTERFACE OpenSSL::Crypto OpenSSL::SSL)
if (MSVC)
target_compile_options(common PRIVATE /bigobj)
target_compile_definitions(common PRIVATE _WIN32_WINNT=0x0601)
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
#=======================================================================
add_executable(cpp20_intro examples/cpp20_intro.cpp)
target_link_libraries(cpp20_intro common)
target_compile_features(cpp20_intro PUBLIC cxx_std_20)
add_test(cpp20_intro cpp20_intro)
if (MSVC)
target_compile_options(cpp20_intro PRIVATE /bigobj)
target_compile_definitions(cpp20_intro PRIVATE _WIN32_WINNT=0x0601)
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()
add_executable(cpp20_intro_awaitable_ops examples/cpp20_intro_awaitable_ops.cpp)
target_link_libraries(cpp20_intro_awaitable_ops common)
target_compile_features(cpp20_intro_awaitable_ops PUBLIC cxx_std_20)
add_test(cpp20_intro_awaitable_ops cpp20_intro_awaitable_ops)
if (MSVC)
target_compile_options(cpp20_intro_awaitable_ops PRIVATE /bigobj)
target_compile_definitions(cpp20_intro_awaitable_ops PRIVATE _WIN32_WINNT=0x0601)
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()
endif()
if (NOT MSVC)
make_example(cpp20_chat_room 20)
endif()
endif()
add_executable(cpp17_intro examples/cpp17_intro.cpp)
target_compile_features(cpp17_intro PUBLIC cxx_std_17)
add_test(cpp17_intro cpp17_intro)
if (MSVC)
target_compile_options(cpp17_intro PRIVATE /bigobj)
target_compile_definitions(cpp17_intro PRIVATE _WIN32_WINNT=0x0601)
endif()
if (BOOST_REDIS_TESTS)
enable_testing()
add_executable(cpp17_intro_sync examples/cpp17_intro_sync.cpp)
target_compile_features(cpp17_intro_sync PUBLIC cxx_std_17)
add_test(cpp17_intro_sync cpp17_intro_sync)
if (MSVC)
target_compile_options(cpp17_intro_sync PRIVATE /bigobj)
target_compile_definitions(cpp17_intro_sync PRIVATE _WIN32_WINNT=0x0601)
endif()
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)
add_executable(cpp20_chat_room examples/cpp20_chat_room.cpp)
target_compile_features(cpp20_chat_room PUBLIC cxx_std_20)
target_link_libraries(cpp20_chat_room common)
if (MSVC)
target_compile_options(cpp20_chat_room PRIVATE /bigobj)
target_compile_definitions(cpp20_chat_room PRIVATE _WIN32_WINNT=0x0601)
endif()
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()
add_executable(cpp20_containers examples/cpp20_containers.cpp)
target_compile_features(cpp20_containers PUBLIC cxx_std_20)
target_link_libraries(cpp20_containers common)
add_test(cpp20_containers cpp20_containers)
if (MSVC)
target_compile_options(cpp20_containers PRIVATE /bigobj)
target_compile_definitions(cpp20_containers PRIVATE _WIN32_WINNT=0x0601)
endif()
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)
add_executable(cpp20_echo_server examples/cpp20_echo_server.cpp)
target_compile_features(cpp20_echo_server PUBLIC cxx_std_20)
target_link_libraries(cpp20_echo_server common)
if (MSVC)
target_compile_options(cpp20_echo_server PRIVATE /bigobj)
target_compile_definitions(cpp20_echo_server PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp20_resolve_with_sentinel examples/cpp20_resolve_with_sentinel.cpp)
target_compile_features(cpp20_resolve_with_sentinel PUBLIC cxx_std_20)
target_link_libraries(cpp20_resolve_with_sentinel common)
#add_test(cpp20_resolve_with_sentinel cpp20_resolve_with_sentinel)
if (MSVC)
target_compile_options(cpp20_resolve_with_sentinel PRIVATE /bigobj)
target_compile_definitions(cpp20_resolve_with_sentinel PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp20_serialization examples/cpp20_serialization.cpp)
target_compile_features(cpp20_serialization PUBLIC cxx_std_20)
target_link_libraries(cpp20_serialization common)
add_test(cpp20_serialization cpp20_serialization)
if (MSVC)
target_compile_options(cpp20_serialization PRIVATE /bigobj)
target_compile_definitions(cpp20_serialization PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp20_subscriber examples/cpp20_subscriber.cpp)
target_compile_features(cpp20_subscriber PUBLIC cxx_std_20)
target_link_libraries(cpp20_subscriber common)
if (MSVC)
target_compile_options(cpp20_subscriber PRIVATE /bigobj)
target_compile_definitions(cpp20_subscriber PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp20_intro_tls examples/cpp20_intro_tls.cpp)
target_compile_features(cpp20_intro_tls PUBLIC cxx_std_20)
add_test(cpp20_intro_tls cpp20_intro_tls)
target_link_libraries(cpp20_intro_tls OpenSSL::Crypto OpenSSL::SSL)
target_link_libraries(cpp20_intro_tls common)
if (MSVC)
target_compile_options(cpp20_intro_tls PRIVATE /bigobj)
target_compile_definitions(cpp20_intro_tls PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp20_low_level_async examples/cpp20_low_level_async.cpp)
target_compile_features(cpp20_low_level_async PUBLIC cxx_std_20)
add_test(cpp20_low_level_async cpp20_low_level_async)
target_link_libraries(cpp20_low_level_async common)
if (MSVC)
target_compile_options(cpp20_low_level_async PRIVATE /bigobj)
target_compile_definitions(cpp20_low_level_async PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(echo_server_client benchmarks/cpp/asio/echo_server_client.cpp)
target_compile_features(echo_server_client PUBLIC cxx_std_20)
if (MSVC)
target_compile_options(echo_server_client PRIVATE /bigobj)
target_compile_definitions(echo_server_client PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(echo_server_direct benchmarks/cpp/asio/echo_server_direct.cpp)
target_compile_features(echo_server_direct PUBLIC cxx_std_20)
if (MSVC)
target_compile_options(echo_server_direct PRIVATE /bigobj)
target_compile_definitions(echo_server_direct PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(cpp17_low_level_sync examples/cpp17_low_level_sync.cpp)
target_compile_features(cpp17_low_level_sync PUBLIC cxx_std_17)
add_test(cpp17_low_level_sync cpp17_low_level_sync)
if (MSVC)
target_compile_options(cpp17_low_level_sync PRIVATE /bigobj)
target_compile_definitions(cpp17_low_level_sync PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_exec tests/conn_exec.cpp)
target_compile_features(test_conn_exec PUBLIC cxx_std_20)
add_test(test_conn_exec test_conn_exec)
if (MSVC)
target_compile_options(test_conn_exec PRIVATE /bigobj)
target_compile_definitions(test_conn_exec PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_exec_retry tests/conn_exec_retry.cpp)
target_compile_features(test_conn_exec_retry PUBLIC cxx_std_20)
add_test(test_conn_exec_retry test_conn_exec_retry)
if (MSVC)
target_compile_options(test_conn_exec_retry PRIVATE /bigobj)
target_compile_definitions(test_conn_exec_retry PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_push tests/conn_push.cpp)
target_compile_features(test_conn_push PUBLIC cxx_std_20)
add_test(test_conn_push test_conn_push)
if (MSVC)
target_compile_options(test_conn_push PRIVATE /bigobj)
target_compile_definitions(test_conn_push PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_quit tests/conn_quit.cpp)
target_compile_features(test_conn_quit PUBLIC cxx_std_17)
add_test(test_conn_quit test_conn_quit)
if (MSVC)
target_compile_options(test_conn_quit PRIVATE /bigobj)
target_compile_definitions(test_conn_quit PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_quit_coalesce tests/conn_quit_coalesce.cpp)
add_test(test_conn_quit_coalesce test_conn_quit_coalesce)
target_compile_features(test_conn_quit_coalesce PUBLIC cxx_std_17)
if (MSVC)
target_compile_options(test_conn_quit_coalesce PRIVATE /bigobj)
target_compile_definitions(test_conn_quit_coalesce PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_reconnect tests/conn_reconnect.cpp)
target_compile_features(test_conn_reconnect PUBLIC cxx_std_20)
target_link_libraries(test_conn_reconnect common)
add_test(test_conn_reconnect test_conn_reconnect)
if (MSVC)
target_compile_options(test_conn_reconnect PRIVATE /bigobj)
target_compile_definitions(test_conn_reconnect PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_tls tests/conn_tls.cpp)
add_test(test_conn_tls test_conn_tls)
target_compile_features(test_conn_tls PUBLIC cxx_std_17)
target_link_libraries(test_conn_tls OpenSSL::Crypto OpenSSL::SSL)
if (MSVC)
target_compile_options(test_conn_tls PRIVATE /bigobj)
target_compile_definitions(test_conn_tls PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_low_level tests/low_level.cpp)
target_compile_features(test_low_level PUBLIC cxx_std_17)
add_test(test_low_level test_low_level)
if (MSVC)
target_compile_options(test_low_level PRIVATE /bigobj)
target_compile_definitions(test_low_level PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_run_cancel tests/conn_run_cancel.cpp)
target_compile_features(test_conn_run_cancel PUBLIC cxx_std_20)
add_test(test_conn_run_cancel test_conn_run_cancel)
if (MSVC)
target_compile_options(test_conn_run_cancel PRIVATE /bigobj)
target_compile_definitions(test_conn_run_cancel PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_exec_cancel tests/conn_exec_cancel.cpp)
target_compile_features(test_conn_exec_cancel PUBLIC cxx_std_20)
target_link_libraries(test_conn_exec_cancel common)
add_test(test_conn_exec_cancel test_conn_exec_cancel)
if (MSVC)
target_compile_options(test_conn_exec_cancel PRIVATE /bigobj)
target_compile_definitions(test_conn_exec_cancel PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_conn_echo_stress tests/conn_echo_stress.cpp)
target_compile_features(test_conn_echo_stress PUBLIC cxx_std_20)
target_link_libraries(test_conn_echo_stress common)
add_test(test_conn_echo_stress test_conn_echo_stress)
if (MSVC)
target_compile_options(test_conn_echo_stress PRIVATE /bigobj)
target_compile_definitions(test_conn_echo_stress PRIVATE _WIN32_WINNT=0x0601)
endif()
add_executable(test_request tests/request.cpp)
target_compile_features(test_request PUBLIC cxx_std_17)
add_test(test_request test_request)
if (MSVC)
target_compile_options(test_request PRIVATE /bigobj)
target_compile_definitions(test_request PRIVATE _WIN32_WINNT=0x0601)
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
#=======================================================================
install(TARGETS aedis
EXPORT aedis
PUBLIC_HEADER DESTINATION include COMPONENT Development
)
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)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.cmake"
COMPATIBILITY AnyNewerVersion
)
configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/AedisConfig.cmake.in"
"${PROJECT_BINARY_DIR}/AedisConfig.cmake"
INSTALL_DESTINATION lib/cmake/aedis
)
install(EXPORT aedis DESTINATION lib/cmake/aedis)
install(FILES "${PROJECT_BINARY_DIR}/AedisConfigVersion.cmake"
"${PROJECT_BINARY_DIR}/AedisConfig.cmake"
DESTINATION lib/cmake/aedis)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include)
include(CPack)
endif()
# Doxygen
#=======================================================================
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
)
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
)
endif()
# Coverage
#=======================================================================
@@ -363,11 +253,6 @@ add_custom_target(
VERBATIM
)
# Distribution
#=======================================================================
include(CPack)
# TODO
#=======================================================================

View File

@@ -40,11 +40,11 @@
}
},
{
"name": "dev",
"name": "g++-11",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/dev",
"binaryDir": "${sourceDir}/build/g++-11",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
@@ -52,15 +52,85 @@
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/dev",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/dev/doc/"
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11/doc/"
}
},
{
"name": "g++-11-release",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/g++-11-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra",
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-release",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11-release/doc/"
}
},
{
"name": "clang++-13",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/clang++-13",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "clang++-13",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-13",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/clang++-13/doc/"
}
},
{
"name": "libc++-14-cpp17",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp17",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp17",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/libc++-14-cpp17/doc/"
}
},
{
"name": "libc++-14-cpp20",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp20",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp20",
"DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/libc++-14-cpp20/doc/"
}
},
{
"name": "clang-tidy",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["dev"],
"inherits": ["g++-11"],
"binaryDir": "${sourceDir}/build/clang-tidy",
"cacheVariables": {
"CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/include/*",
@@ -70,7 +140,11 @@
],
"buildPresets": [
{ "name": "coverage", "configurePreset": "coverage" },
{ "name": "dev", "configurePreset": "dev" },
{ "name": "g++-11", "configurePreset": "g++-11" },
{ "name": "g++-11-release", "configurePreset": "g++-11-release" },
{ "name": "clang++-13", "configurePreset": "clang++-13" },
{ "name": "libc++-14-cpp17", "configurePreset": "libc++-14-cpp17" },
{ "name": "libc++-14-cpp20", "configurePreset": "libc++-14-cpp20" },
{ "name": "clang-tidy", "configurePreset": "clang-tidy" }
],
"testPresets": [
@@ -80,8 +154,12 @@
"output": {"outputOnFailure": true},
"execution": {"noTestsAction": "error", "stopOnFailure": true}
},
{ "name": "coverage", "configurePreset": "coverage", "inherits": ["test"] },
{ "name": "dev", "configurePreset": "dev", "inherits": ["test"] },
{ "name": "clang-tidy", "configurePreset": "clang-tidy", "inherits": ["test"] }
{ "name": "coverage", "configurePreset": "coverage", "inherits": ["test"] },
{ "name": "g++-11", "configurePreset": "g++-11", "inherits": ["test"] },
{ "name": "g++-11-release", "configurePreset": "g++-11-release", "inherits": ["test"] },
{ "name": "clang++-13", "configurePreset": "clang++-13", "inherits": ["test"] },
{ "name": "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"] }
]
}

637
README.md
View File

@@ -1,166 +1,76 @@
# Aedis
# boost_redis
Aedis is a [Redis](https://redis.io/) client library built on top of
Boost.Redis is a high-level [Redis](https://redis.io/) client library built on top of
[Boost.Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html)
that implements the latest version of the Redis communication
protocol
that implements Redis plain text protocol
[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md).
It makes communication with a Redis server easy by hiding low-level
code away from the user, which, in the majority of the cases will be
concerned with only three library entities
It can multiplex any number of client
requests, responses, and server pushes onto a single active socket
connection to the Redis server. The requirements for using Boost.Redis are
* `aedis::connection`: A connection to the Redis server with
high-level functions to execute Redis commands, receive server
pushes and support for automatic command
[pipelines](https://redis.io/docs/manual/pipelining/).
* `aedis::resp3::request`: A container of Redis commands that supports
STL containers and user defined data types.
* `aedis::adapt()`: A function that adapts data structures to receive responses.
In the next sections we will cover all those points in detail with
examples. The requirements for using Aedis are
* Boost 1.80 or greater.
* Boost 1.81 or greater.
* C++17 minimum.
* Redis 6 or higher (must support RESP3).
* Gcc (10, 11, 12), Clang (11, 13, 14) and Visual Studio (16 2019, 17 2022).
* Have basic-level knowledge about Redis and understand Asio and its asynchronous model.
* Have basic-level knowledge about [Redis](https://redis.io/docs/)
and [Boost.Asio](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/overview.html).
Readers that are not familiar with Redis can learn more about
it on https://redis.io/docs/, in essence
The latest release can be downloaded on
https://github.com/boostorg/redis/releases. The library headers can be
found in the `include` subdirectory and a compilation of the source
> Redis is an open source (BSD licensed), in-memory data structure
> store used as a database, cache, message broker, and streaming
> engine. Redis provides data structures such as strings, hashes,
> lists, sets, sorted sets with range queries, bitmaps, hyperloglogs,
> geospatial indexes, and streams. Redis has built-in replication, Lua
> scripting, LRU eviction, transactions, and different levels of
> on-disk persistence, and provides high availability via Redis
> Sentinel and automatic partitioning with Redis Cluster.
```cpp
#include <boost/redis/src.hpp>
```
is required. The simplest way to do it is to included this header in
no more than one source file in your applications. To build the
examples and tests cmake is supported, for example
```cpp
# Linux
$ BOOST_ROOT=/opt/boost_1_81_0 cmake --preset g++-11
# Windows
$ cmake -G "Visual Studio 17 2022" -A x64 -B bin64 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
```
<a name="connection"></a>
## Connection
Let us start with a simple application that uses a short-lived
connection to read Redis
[hashes](https://redis.io/docs/data-types/hashes/) in a `std::map`
connection to send a [ping](https://redis.io/commands/ping/) command
to Redis
```cpp
auto co_main() -> net::awaitable<void>
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));
// From examples/common.hpp to avoid vebosity
co_await connect(conn, "127.0.0.1", "6379");
// A request can contain multiple commands.
resp3::request req;
req.push("HELLO", 3);
req.push("HGETALL", "hset-key");
req.push("QUIT");
// The tuple elements will store the responses to each individual
// command. The responses to HELLO and QUIT are being ignored for
// simplicity.
std::tuple<ignore, std::map<std::string, std::string>, ignore> resp;
// Executes the request. See below why we are using operator ||.
co_await (conn->async_run() || conn->async_exec(req, adapt(resp)));
// Use the map from std::get<1>(resp) ...
}
```
The example above uses the Asio awaitable `operator ||` to compose
`connection::async_exec` and `connection::async_run` in an
operation we can `co_await` on. It also provides cancelation of one of
the operations when the other completes. The role played by these
functions are
* `connection::async_exec`: Execute commands by queuing the request
for writing and wait for the response sent back by
Redis. Can be called from multiple places in your code concurrently.
* `connection::async_run`: Coordinate low-level read and write
operations. More specifically, it will hand IO control to
`async_exec` when a response arrives and to `async_receive` when a
server-push is received. It is also responsible for triggering writes of pending
requests.
The example above is also available in other programming styles for comparison
* cpp20_intro_awaitable_ops.cpp: The version shown above.
* cpp20_intro.cpp: Does not use awaitable operators.
* cpp20_intro_tls.cpp: Communicates over TLS.
* cpp17_intro.cpp: Uses callbacks and requires C++17.
* cpp17_intro_sync.cpp: Runs `async_run` in a separate thread and
performs synchronous calls to `async_exec`.
For performance reasons we will usually want to perform multiple
requests with the same connection. We can do this in the example above
by letting `async_run` run detached in a separate coroutine, for
example (see cpp20_intro.cpp)
```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
co_await connect(conn, "127.0.0.1", "6379");
co_await conn->async_run();
}
auto hello(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("HELLO", 3);
co_await conn->async_exec(req);
}
auto ping(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
// A request containing only a ping command.
request req;
req.push("PING", "Hello world");
std::tuple<std::string> resp;
co_await conn->async_exec(req, adapt(resp));
// Use the response ...
}
// Response where the PONG response will be stored.
response<std::string> resp;
auto quit(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("QUIT");
// Executes the request.
co_await conn->async_exec(req, resp, net::deferred);
conn->cancel();
co_await conn->async_exec(req);
}
auto co_main() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
net::co_spawn(ex, run(conn), net::detached);
co_await hello(conn);
co_await ping(conn);
co_await quit(conn);
// conn can be passed around to other coroutines that need to
// communicate with Redis. For example, sessions in a HTTP and
// Websocket server.
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
```
With this separation, it is now easy to incorporate other long-running
operations in our application, for example, the run coroutine below
adds signal handling and a healthy checker (see cpp20_echo_server.cpp)
The roles played by the `async_run` and `async_exec` functions are
```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
signal_set sig{co_await net::this_coro::executor, SIGINT, SIGTERM};
co_await connect(conn, "127.0.0.1", "6379");
co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn));
}
```
The definition of the `healthy_checker` used above can be found in common.cpp.
* `async_exec`: Execute the commands contained in the
request and store the individual responses in the `resp` object. Can
be called from multiple places in your code concurrently.
* `async_run`: Resolve, connect, ssl-handshake,
resp3-handshake, health-checks, reconnection and coordinate low-level
read and write operations (among other things).
### Server pushes
@@ -172,101 +82,39 @@ them are
* [Client-side caching](https://redis.io/docs/manual/client-side-caching/)
The connection class supports server pushes by means of the
`aedis::connection::async_receive` function, the coroutine shows how
`boost::redis::connection::async_receive` function, the coroutine shows how
to used it
```cpp
auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
auto
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
using resp_type = std::vector<resp3::node<std::string>>;
for (resp_type resp;;) {
co_await conn->async_receive(adapt(resp));
// Use resp and clear the response for a new push.
resp.clear();
request req;
req.push("SUBSCRIBE", "channel");
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req, ignore, net::deferred);
// Loop reading Redis pushes.
for (generic_response resp;;) {
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.
...
resp.value().clear();
}
}
}
```
The `receiver` defined above can be run detached or incorporated
in the `run` function as we did for the `healthy_checker`
```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
signal_set sig{co_await net::this_coro::executor, SIGINT, SIGTERM};
co_await connect(conn, "127.0.0.1", "6379");
co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn) || receiver(conn));
}
```
### Reconnecting
Adding a loop around `async_run` produces a simple way to support
reconnection _while there are pending operations on the connection_,
for example, to reconnect to the same address (see cpp20_subscriber.cpp)
```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
steady_timer timer{co_await net::this_coro::executor};
for (;;) {
co_await connect(conn, "127.0.0.1", "6379");
co_await (conn->async_run() || healthy_checker(conn) || receiver(conn));
// Prepare the stream for a new connection.
conn->reset_stream();
// Waits one second before trying to reconnect.
timer.expires_after(std::chrono::seconds{1});
co_await timer.async_wait();
}
}
```
This feature results in considerable simplification of backend code
and makes it easier to write failover-safe applications. For example,
a Websocket server might have a 10k sessions communicating with Redis at
the time the connection is lost (or maybe killed by the server admin
to force a failover). It would be concerning if each individual section
were to throw exceptions and handle error. With the pattern shown
above the only place that has to manage the error is the run function.
### Cancellation
Aedis supports both implicit and explicit cancellation of connection
operations. Explicit cancellation is supported by means of the
`aedis::connection::cancel` member function. Implicit
terminal-cancellation, like those that happen when using Asio
awaitable `operator ||` will be discussed with more detail below.
```cpp
co_await (conn.async_run(...) || conn.async_exec(...))
```
* Useful for short-lived connections that are meant to be closed after
a command has been executed.
```cpp
co_await (conn.async_exec(...) || time.async_wait(...))
```
* Provides a way to limit how long the execution of a single request
should last.
* WARNING: If the timer fires after the request has been sent but before the
response has been received, the connection will be closed.
* It is usually a better idea to have a healthy checker than adding
per request timeout, see cpp20_subscriber.cpp for an example.
```cpp
co_await (conn.async_exec(...) || conn.async_exec(...) || ... || conn.async_exec(...))
```
* This works but is unnecessary. Unless the user has set
`aedis::resp3::request::config::coalesce` to `false`, and he
usually shouldn't, the connection will automatically merge the
individual requests into a single payload.
<a name="requests"></a>
## Requests
@@ -279,7 +127,7 @@ Redis documentation they are called
std::list<std::string> list {...};
std::map<std::string, mystruct> map { ...};
// The request can contains multiple commands.
// The request can contain multiple commands.
request req;
// Command with variable length of arguments.
@@ -295,58 +143,23 @@ req.push_range("SUBSCRIBE", std::cbegin(list), std::cend(list));
req.push_range("HSET", "key", map);
```
Sending a request to Redis is performed with `aedis::connection::async_exec` as already stated.
<a name="serialization"></a>
### Serialization
The `resp3::request::push` and `resp3::request::push_range` member functions work
with integer data types e.g. `int` and `std::string` out of the box.
To send your own data type define a `to_bulk` function like this
```cpp
// User defined type.
struct mystruct {...};
// Serialize it in to_bulk.
void to_bulk(std::pmr::string& to, mystruct const& obj)
{
std::string dummy = "Dummy serializaiton string.";
aedis::resp3::to_bulk(to, dummy);
}
```
Once `to_bulk` is defined and visible over ADL `mystruct` can
be passed to the `request`
```cpp
request req;
std::map<std::string, mystruct> map {...};
req.push_range("HSET", "key", map);
```
Example cpp20_serialization.cpp shows how store json strings in Redis.
<a name="responses"></a>
Sending a request to Redis is performed with `boost::redis::connection::async_exec` as already stated.
### Config flags
The `aedis::resp3::request::config` object inside the request dictates how the
`aedis::connection` should handle the request in some important situations. The
The `boost::redis::request::config` object inside the request dictates how the
`boost::redis::connection` should handle the request in some important situations. The
reader is advised to read it carefully.
<a name="responses"></a>
## Responses
Aedis uses the following strategy to support Redis responses
Boost.Redis uses the following strategy to support Redis responses
* **Static**: For `aedis::resp3::request` whose sizes are known at compile time
`std::tuple` is supported.
* **Dynamic**: Otherwise use `std::vector<aedis::resp3::node<std::string>>`.
* `boost::redis::request` is used for requests whose number of commands are not dynamic.
* **Dynamic**: Otherwise use `boost::redis::generic_response`.
For example, below is a request with a compile time size
For example, the request below has three commands
```cpp
request req;
@@ -355,22 +168,23 @@ req.push("INCR", "key");
req.push("QUIT");
```
To read the response to this request users can use the following tuple
and its response also has three comamnds and can be read in the
following response object
```cpp
std::tuple<std::string, int, std::string>
response<std::string, int, std::string>
```
The pattern might have become apparent to the reader: the tuple must
The response behaves as a 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
`aedis::ignore`
`boost::redis::ignore_t`, for example
```cpp
// Ignore the second and last responses.
std::tuple<std::string, aedis::ignore, std::string, aedis::ignore>
response<std::string, boost::redis::ignore_t, std::string, boost::redis::ignore_t>
```
The following table provides the resp3-types returned by some Redis
@@ -417,31 +231,27 @@ req.push("QUIT");
can be read in the tuple below
```cpp
std::tuple<
aedis::ignore, // hello
int, // rpush
int, // hset
std::vector<T>, // lrange
std::map<U, V>, // hgetall
std::string // quit
response<
redis::ignore_t, // hello
int, // rpush
int, // hset
std::vector<T>, // lrange
std::map<U, V>, // hgetall
std::string // quit
> resp;
```
Where both are passed to `async_exec` as showed elsewhere
```cpp
co_await conn->async_exec(req, adapt(resp));
co_await conn->async_exec(req, resp, net::deferred);
```
If the intention is to ignore the response to all commands altogether
use `adapt()` without arguments instead
If the intention is to ignore responses altogether use `ignore`
```cpp
// Uses the ignore adapter explicitly.
co_await conn->async_exec(req, adapt());
// Ignore adapter is also the default argument.
co_await conn->async_exec(req);
// Ignores the response
co_await conn->async_exec(req, ignore, net::deferred);
```
Responses that contain nested aggregates or heterogeneous data
@@ -458,7 +268,7 @@ Commands that have no response like
* `"PSUBSCRIBE"`
* `"UNSUBSCRIBE"`
must be **NOT** be included in the response tuple. For example, the request below
must **NOT** be included in the response tuple. For example, the request below
```cpp
request req;
@@ -467,34 +277,34 @@ req.push("SUBSCRIBE", "channel");
req.push("QUIT");
```
must be read in this tuple `std::tuple<std::string, std::string>`,
that has size two.
must be read in this tuple `response<std::string, std::string>`,
that has static size two.
### 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
cases Aedis provides support for `std::optional`. To use it,
cases Boost.Redis provides support for `std::optional`. To use it,
wrap your type around `std::optional` like this
```cpp
std::tuple<
response<
std::optional<A>,
std::optional<B>,
...
> resp;
co_await conn->async_exec(req, adapt(resp));
co_await conn->async_exec(req, resp, net::deferred);
```
Everything else stays pretty much 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 is itself the response to the `EXEC` command.
For example, to read the response to this request
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 is itself the response to
the `EXEC` command. For example, to read the response to this request
```cpp
req.push("MULTI");
@@ -507,47 +317,28 @@ req.push("EXEC");
use the following response type
```cpp
using aedis::ignore;
using boost::redis::ignore;
using exec_resp_type =
std::tuple<
response<
std::optional<std::string>, // get
std::optional<std::vector<std::string>>, // lrange
std::optional<std::map<std::string, std::string>> // hgetall
>;
std::tuple<
aedis::ignore, // multi
aedis::ignore, // get
aedis::ignore, // lrange
aedis::ignore, // hgetall
exec_resp_type, // exec
response<
boost::redis::ignore_t, // multi
boost::redis::ignore_t, // get
boost::redis::ignore_t, // lrange
boost::redis::ignore_t, // hgetall
exec_resp_type, // exec
> resp;
co_await conn->async_exec(req, adapt(resp));
co_await conn->async_exec(req, resp, net::deferred);
```
For a complete example see cpp20_containers.cpp.
### Deserialization
As mentioned in the serialization section, it is common practice to
serialize data before sending it to Redis e.g. as json strings. For
performance and convenience reasons, we may also want to deserialize
responses directly in their final data structure. Aedis supports this
use case by calling a user provided `from_bulk` function while parsing
the response. For example
```cpp
void from_bulk(mystruct& obj, char const* p, std::size_t size, boost::system::error_code& ec)
{
// Deserializes p into obj.
}
```
After that, you can start receiving data efficiently in the desired
types e.g. `mystruct`, `std::map<std::string, mystruct>` etc.
<a name="the-general-case"></a>
### The general case
@@ -556,19 +347,19 @@ 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
will result in error.
RESP3 type. Expecting an `int` and receiving a blob-string
will result in 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 `std::tuple`.
* Transactions with a dynamic number of commands can't be read in a `response`.
To deal with these cases Aedis provides the `aedis::resp3::node` type
To deal with these cases Boost.Redis provides the `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 this
```cpp
template <class String>
struct node {
struct basic_node {
// The RESP3 type of the data in this node.
type data_type;
@@ -584,49 +375,67 @@ struct node {
```
Any response to a Redis command can be received in a
`std::vector<node<std::string>>`. The vector can be seen as a
`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
```cpp
// Receives any RESP3 simple or aggregate data type.
std::vector<node<std::string>> resp;
co_await conn->async_exec(req, adapt(resp));
boost::redis::generic_response resp;
co_await conn->async_exec(req, resp, net::deferred);
```
For example, suppose we want to retrieve a hash data structure
from Redis with `HGETALL`, some of the options are
* `std::vector<node<std::string>`: Works always.
* `boost::redis::generic_response`: Works always.
* `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 `from_bulk` for `U` and `V`.
* `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.
<a name="serialization"></a>
## Serialization
Boost.Redis supports serialization of user defined types by means of
the following customization points
```cpp
// Serialize.
void boost_redis_to_bulk(std::string& to, mystruct const& obj);
// Deserialize
void boost_redis_from_bulk(mystruct& obj, char const* p, std::size_t size, boost::system::error_code& ec)
```
These functions are accessed over ADL and therefore they must be
imported in the global namespace by the user. In the
[Examples](#examples) section the reader can find examples showing how
to serialize using json and [protobuf](https://protobuf.dev/).
<a name="examples"></a>
## Examples
The examples below show how to use the features discussed so far
* cpp20_intro_awaitable_ops.cpp: The version shown above.
* cpp20_intro.cpp: Does not use awaitable operators.
* cpp20_intro_tls.cpp: Communicates over TLS.
* cpp20_containers.cpp: Shows how to send and receive STL containers and how to use transactions.
* cpp20_serialization.cpp: Shows how to serialize types using Boost.Json.
* cpp20_json.cpp: Shows how to serialize types using Boost.Json.
* cpp20_protobuf.cpp: Shows how to serialize types using protobuf.
* cpp20_resolve_with_sentinel.cpp: Shows how to resolve a master address using sentinels.
* cpp20_subscriber.cpp: Shows how to implement pubsub with reconnection re-subscription.
* cpp20_echo_server.cpp: A simple TCP echo server.
* cpp20_chat_room.cpp: A command line chat built on Redis pubsub.
* cpp20_low_level_async.cpp: Sends a ping asynchronously using the low-level API.
* cpp17_low_level_sync.cpp: Sends a ping synchronously using the low-level API.
* cpp17_intro.cpp: Uses callbacks and requires C++17.
* cpp17_intro_sync.cpp: Runs `async_run` in a separate thread and performs synchronous calls to `async_exec`.
To avoid repetition code that is common to some examples has been
grouped in common.hpp. The main function used in some async examples
has been factored out in the main.cpp file.
The main function used in some async examples has been factored out in
the main.cpp file.
## Echo server benchmark
@@ -647,7 +456,7 @@ I also imposed some constraints on the implementations
To reproduce these results run one of the echo-server programs in one
terminal and the
[echo-server-client](https://github.com/mzimbres/aedis/blob/42880e788bec6020dd018194075a211ad9f339e8/benchmarks/cpp/asio/echo_server_client.cpp)
[echo-server-client](https://github.com/boostorg/redis/blob/42880e788bec6020dd018194075a211ad9f339e8/benchmarks/cpp/asio/echo_server_client.cpp)
in another.
### Without Redis
@@ -656,7 +465,7 @@ 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
![](https://mzimbres.github.io/aedis/tcp-echo-direct.png)
![](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
@@ -671,11 +480,11 @@ decrease.
The code used in the benchmarks can be found at
* [Asio](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/cpp/asio/echo_server_direct.cpp): A variation of [this](https://github.com/chriskohlhoff/asio/blob/4915cfd8a1653c157a1480162ae5601318553eb8/asio/src/examples/cpp20/coroutines/echo_server.cpp) Asio example.
* [Libuv](https://github.com/mzimbres/aedis/tree/835a1decf477b09317f391eddd0727213cdbe12b/benchmarks/c/libuv): Taken from [here](https://github.com/libuv/libuv/blob/06948c6ee502862524f233af4e2c3e4ca876f5f6/docs/code/tcp-echo-server/main.c) Libuv example .
* [Tokio](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/rust/echo_server_direct): Taken from [here](https://docs.rs/tokio/latest/tokio/).
* [Nodejs](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_direct)
* [Go](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_direct.go)
* [Asio](https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/cpp/asio/echo_server_direct.cpp): A variation of [this](https://github.com/chriskohlhoff/asio/blob/4915cfd8a1653c157a1480162ae5601318553eb8/asio/src/examples/cpp20/coroutines/echo_server.cpp) Asio example.
* [Libuv](https://github.com/boostorg/redis/tree/835a1decf477b09317f391eddd0727213cdbe12b/benchmarks/c/libuv): Taken from [here](https://github.com/libuv/libuv/blob/06948c6ee502862524f233af4e2c3e4ca876f5f6/docs/code/tcp-echo-server/main.c) Libuv example .
* [Tokio](https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/rust/echo_server_direct): Taken from [here](https://docs.rs/tokio/latest/tokio/).
* [Nodejs](https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_direct)
* [Go](https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_direct.go)
### With Redis
@@ -684,7 +493,7 @@ 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
![](https://mzimbres.github.io/aedis/tcp-echo-over-redis.png)
![](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
@@ -708,17 +517,17 @@ in the graph, the reasons are
The code used in the benchmarks can be found at
* [Aedis](https://github.com/mzimbres/aedis): [code](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/examples/echo_server.cpp)
* [node-redis](https://github.com/redis/node-redis): [code](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_over_redis)
* [go-redis](https://github.com/go-redis/redis): [code](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_over_redis.go)
* [Boost.Redis](https://github.com/boostorg/redis): [code](https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/examples/echo_server.cpp)
* [node-redis](https://github.com/redis/node-redis): [code](https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_over_redis)
* [go-redis](https://github.com/go-redis/redis): [code](https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_over_redis.go)
### Conclusion
Redis clients have to support automatic pipelining to have competitive performance. For updates to this document follow https://github.com/mzimbres/aedis.
Redis clients have to support automatic pipelining to have competitive performance. For updates to this document follow https://github.com/boostorg/redis.
## Comparison
The main reason for why I started writing Aedis was to have a client
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
@@ -729,7 +538,7 @@ stars, namely
* https://github.com/sewenew/redis-plus-plus
### Aedis vs 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
@@ -799,7 +608,7 @@ Transactions also suffer from the very same problem.
> NOTE: Creating a Transaction object is NOT cheap, since it
> creates a new connection.
In Aedis there is no difference between sending one command, a
In Boost.Redis there is no difference between sending one command, a
pipeline or a transaction because requests are decoupled
from the IO objects.
@@ -830,43 +639,99 @@ It is also not clear how are pipelines realised with this design
<a name="api-reference"></a>
## Reference
* [High-Level](#high-level-api): Covers the topics discussed in this document.
* [Low-Level](#low-level-api): Covers low-level building blocks. Provided mostly for developers, users won't usually need any information provided here.
The [High-Level](#high-level-api) page documents all public types.
## Installation
Download the latest release on
https://github.com/mzimbres/aedis/releases. Aedis is a header only
library, so you can starting using it right away by adding the
`include` subdirectory to your project and including
```cpp
#include <aedis/src.hpp>
```
in no more than one source file in your applications. To build the
examples and tests cmake is supported, for example
```cpp
BOOST_ROOT=/opt/boost_1_80_0 cmake --preset dev
```
## Acknowledgement
Acknowledgement to people that helped shape Aedis
Acknowledgement to people that helped shape Boost.Redis
* Richard Hodges ([madmongo1](https://github.com/madmongo1)): For very helpful support with Asio, the design of asynchronous programs, etc.
* Vinícius dos Santos Oliveira ([vinipsmaker](https://github.com/vinipsmaker)): For useful discussion about how Aedis consumes buffers in the read operation.
* Vinícius dos Santos Oliveira ([vinipsmaker](https://github.com/vinipsmaker)): For useful discussion about how Boost.Redis consumes buffers in the read operation.
* Petr Dannhofer ([Eddie-cz](https://github.com/Eddie-cz)): For helping me understand how the `AUTH` and `HELLO` command can influence each other.
* Mohammad Nejati ([ashtum](https://github.com/ashtum)): For pointing out scenarios where calls to `async_exec` should fail when the connection is lost.
* Klemens Morgenstern ([klemens-morgenstern](https://github.com/klemens-morgenstern)): For useful discussion about timeouts, cancellation, synchronous interfaces and general help with Asio.
* Vinnie Falco ([vinniefalco](https://github.com/vinniefalco)): For general suggestions about how to improve the code and the documentation.
* Bram Veldhoen ([bveldhoen](https://github.com/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.
## Changelog
### develop (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 theses 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 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.
* Removes dependency on Boost.Hana, `boost::string_view`, Boost.Variant2 and Boost.Spirit.
* Fixes build and setup CI on windows.
### v1.3.0-1
@@ -877,7 +742,7 @@ Acknowledgement to people that helped shape Aedis
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::resp3::request::config`
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
@@ -908,21 +773,21 @@ Acknowledgement to people that helped shape Aedis
asio::error::eof is received. This makes it easier to write
composed operations with awaitable operators.
* Adds allocator support in the `aedis::resp3::request` (a
* Adds allocator support in the `aedis::request` (a
contribution from Klemens Morgenstern).
* Renames `aedis::resp3::request::push_range2` to `push_range`. The
* 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::resp3::request::config::cancel_on_connection_lost`. Now, it will
`aedis::request::config::cancel_on_connection_lost`. Now, it will
only cause connections to be canceled when `async_run` completes.
* Introduces `aedis::resp3::request::config::cancel_if_not_connected` which will
* 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::resp3::request::config::retry` that if
* 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.
@@ -952,7 +817,7 @@ Acknowledgement to people that helped shape Aedis
### v1.1.0-1
* Removes `coalesce_requests` from the `aedis::connection::config`, it
became a request property now, see `aedis::resp3::request::config::coalesce`.
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

View File

@@ -0,0 +1,4 @@
$ npm install
$ node echo_server_over_redis.js

View File

@@ -1,7 +1,7 @@
import { createClient } from 'redis';
import * as net from 'net';
const client = createClient({url: 'redis://db.occase.de:6379' });
const client = createClient({url: 'redis://aedis.occase.de:63799' });
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();

View File

@@ -4,4 +4,4 @@
* accompanying file LICENSE.txt)
*/
#include <aedis/src.hpp>
#include <boost/redis/src.hpp>

View File

@@ -1,93 +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 "common.hpp"
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
namespace net = boost::asio;
using namespace net::experimental::awaitable_operators;
using resolver = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::resolver>;
using timer_type = net::use_awaitable_t<>::as_default_on_t<net::steady_timer>;
using aedis::resp3::request;
using aedis::adapt;
using aedis::operation;
namespace
{
auto redir(boost::system::error_code& ec)
{ return net::redirect_error(net::use_awaitable, ec); }
}
auto healthy_checker(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
try {
request req;
req.push("PING");
timer_type timer{co_await net::this_coro::executor};
for (boost::system::error_code ec;;) {
timer.expires_after(std::chrono::seconds{1});
co_await (conn->async_exec(req, adapt()) || timer.async_wait(redir(ec)));
if (!ec) {
co_return;
}
// Waits some time before trying the next ping.
timer.expires_after(std::chrono::seconds{1});
co_await timer.async_wait();
}
} catch (...) {
}
}
auto
connect(
std::shared_ptr<connection> conn,
std::string const& host,
std::string const& port) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
resolver resv{ex};
timer_type timer{ex};
boost::system::error_code ec;
timer.expires_after(std::chrono::seconds{5});
auto const addrs = co_await (resv.async_resolve(host, port) || timer.async_wait(redir(ec)));
if (!ec)
throw std::runtime_error("Resolve timeout");
timer.expires_after(std::chrono::seconds{5});
co_await (net::async_connect(conn->next_layer(), std::get<0>(addrs)) || timer.async_wait(redir(ec)));
if (!ec)
throw std::runtime_error("Connect timeout");
}
auto run(net::awaitable<void> op) -> int
{
try {
net::io_context ioc;
net::co_spawn(ioc, std::move(op), [](std::exception_ptr p) {
if (p)
std::rethrow_exception(p);
});
ioc.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 1;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,34 +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 AEDIS_EXAMPLES_COMMON_HPP
#define AEDIS_EXAMPLES_COMMON_HPP
#include <boost/asio.hpp>
#include <aedis.hpp>
#include <memory>
#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <string>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
using connection = boost::asio::use_awaitable_t<>::as_default_on_t<aedis::connection>;
auto
connect(
std::shared_ptr<connection> conn,
std::string const& host,
std::string const& port) -> boost::asio::awaitable<void>;
auto healthy_checker(std::shared_ptr<connection> conn) -> boost::asio::awaitable<void>;
auto run(boost::asio::awaitable<void> op) -> int;
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // AEDIS_EXAMPLES_COMMON_HPP

View File

@@ -1,38 +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/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include "common.hpp"
extern boost::asio::awaitable<void> co_main(std::string, std::string);
auto main(int argc, char * argv[]) -> int
{
std::string host = "127.0.0.1";
std::string port = "6379";
if (argc == 3) {
host = argv[1];
port = argv[2];
}
return run(co_main(host, port));
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <iostream>
auto main() -> int
{
std::cout << "Requires coroutine support." << std::endl;
return 0;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -4,98 +4,47 @@
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#include <boost/asio.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using aedis::resp3::request;
using aedis::adapt;
using aedis::operation;
void log(boost::system::error_code const& ec, char const* prefix)
{
std::clog << prefix << ec.message() << std::endl;
}
using boost::redis::connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
auto main(int argc, char * argv[]) -> int
{
try {
std::string host = "127.0.0.1";
std::string port = "6379";
config cfg;
if (argc == 3) {
host = argv[1];
port = argv[2];
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
// The request
resp3::request req;
req.push("HELLO", 3);
request req;
req.push("PING", "Hello world");
req.push("QUIT");
// The response.
std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
response<std::string> resp;
net::io_context ioc;
connection conn{ioc};
// IO objects.
net::ip::tcp::resolver resv{ioc};
aedis::connection conn{ioc};
conn.async_run(cfg, {}, net::detached);
// Resolve endpoints.
net::ip::tcp::resolver::results_type endpoints;
// async_run callback.
auto on_run = [](auto ec)
{
if (ec)
return log(ec, "on_run: ");
};
// async_exec callback.
auto on_exec = [&](auto ec, auto)
{
if (ec) {
conn.cancel(operation::run);
return log(ec, "on_exec: ");
}
std::cout << "PING: " << std::get<1>(resp) << std::endl;
};
// Connect callback.
auto on_connect = [&](auto ec, auto)
{
if (ec)
return log(ec, "on_connect: ");
conn.async_run(on_run);
conn.async_exec(req, adapt(resp), on_exec);
};
// Resolve callback.
auto on_resolve = [&](auto ec, auto const& addrs)
{
if (ec)
return log(ec, "on_resolve: ");
endpoints = addrs;
net::async_connect(conn.next_layer(), endpoints, on_connect);
};
resv.async_resolve(host, port, on_resolve);
conn.async_exec(req, resp, [&](auto ec, auto) {
if (!ec)
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
conn.cancel();
});
ioc.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 1;
}

View File

@@ -4,75 +4,40 @@
* accompanying file LICENSE.txt)
*/
#include <tuple>
#include <string>
#include <thread>
#include <iostream>
#include <boost/asio.hpp>
#include <aedis.hpp>
#include "sync_connection.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
#include <string>
#include <iostream>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using aedis::adapt;
using connection = aedis::connection;
template <class Adapter>
auto exec(std::shared_ptr<connection> conn, resp3::request const& req, Adapter adapter)
{
net::dispatch(
conn->get_executor(),
net::deferred([&]() { return conn->async_exec(req, adapter, net::deferred); }))
(net::use_future).get();
}
auto logger = [](auto const& ec)
{ std::clog << "Run: " << ec.message() << std::endl; };
using boost::redis::sync_connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
auto main(int argc, char * argv[]) -> int
{
try {
std::string host = "127.0.0.1";
std::string port = "6379";
config cfg;
if (argc == 3) {
host = argv[1];
port = argv[2];
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
net::io_context ioc{1};
sync_connection conn;
conn.run(cfg);
auto conn = std::make_shared<connection>(ioc);
// Resolves the address
net::ip::tcp::resolver resv{ioc};
auto const res = resv.resolve(host, port);
// Connect to Redis
net::connect(conn->next_layer(), res);
// Starts a thread that will can io_context::run on which
// the connection will run.
std::thread t{[conn, &ioc]() {
conn->async_run(logger);
ioc.run();
}};
resp3::request req;
req.push("HELLO", 3);
request req;
req.push("PING");
req.push("QUIT");
std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
response<std::string> resp;
// Executes commands synchronously.
exec(conn, req, adapt(resp));
conn.exec(req, resp);
conn.stop();
std::cout << "Response: " << std::get<1>(resp) << std::endl;
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
t.join();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}

View File

@@ -1,58 +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 <string>
#include <iostream>
#include <boost/asio/connect.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using aedis::adapter::adapt2;
auto main(int argc, char * argv[]) -> int
{
try {
std::string host = "127.0.0.1";
std::string port = "6379";
if (argc == 3) {
host = argv[1];
port = argv[2];
}
net::io_context ioc;
net::ip::tcp::resolver resv{ioc};
auto const res = resv.resolve(host, port);
net::ip::tcp::socket socket{ioc};
net::connect(socket, res);
// Creates the request and writes to the socket.
resp3::request req;
req.push("HELLO", 3);
req.push("PING", "Hello world");
req.push("QUIT");
resp3::write(socket, req);
// Responses
std::string buffer, resp;
// Reads the responses to all commands in the request.
auto dbuffer = net::dynamic_buffer(buffer);
resp3::read(socket, dbuffer);
resp3::read(socket, dbuffer, adapt2(resp));
resp3::read(socket, dbuffer);
std::cout << "Ping: " << resp << std::endl;
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
exit(EXIT_FAILURE);
}
}

View File

@@ -4,33 +4,59 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <iostream>
namespace net = boost::asio;
#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#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 <iostream>
#include "common/common.hpp"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using stream_descriptor = net::use_awaitable_t<>::as_default_on_t<net::posix::stream_descriptor>;
using signal_set = net::use_awaitable_t<>::as_default_on_t<net::signal_set>;
using aedis::adapt;
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;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::ignore;
using net::redirect_error;
using net::use_awaitable;
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.
// Receives Redis pushes.
auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
auto
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
for (std::vector<resp3::node<std::string>> resp;;) {
co_await conn->async_receive(adapt(resp));
std::cout << resp.at(1).value << " " << resp.at(2).value << " " << resp.at(3).value << std::endl;
resp.clear();
request req;
req.push("SUBSCRIBE", "channel");
while (conn->will_reconnect()) {
// Subscribe to channels.
co_await conn->async_exec(req, ignore, net::deferred);
// Loop reading Redis push messages.
for (generic_response resp;;) {
error_code ec;
co_await conn->async_receive(resp, redirect_error(use_awaitable, ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
std::cout
<< resp.value().at(1).value
<< " " << resp.value().at(2).value
<< " " << resp.value().at(3).value
<< std::endl;
resp.value().clear();
}
}
}
@@ -39,32 +65,32 @@ auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection
{
for (std::string msg;;) {
auto n = co_await net::async_read_until(*in, net::dynamic_buffer(msg, 1024), "\n");
resp3::request req;
req.push("PUBLISH", "chat-channel", msg);
co_await conn->async_exec(req);
request req;
req.push("PUBLISH", "channel", msg);
co_await conn->async_exec(req, ignore, net::deferred);
msg.erase(0, n);
}
}
// Called from the main function (see main.cpp)
auto co_main(std::string host, std::string port) -> net::awaitable<void>
auto co_main(config cfg) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
signal_set sig{ex, SIGINT, SIGTERM};
resp3::request req;
req.push("HELLO", 3);
req.push("SUBSCRIBE", "chat-channel");
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_await connect(conn, host, port);
co_await ((conn->async_run() || publisher(stream, conn) || receiver(conn) ||
healthy_checker(conn) || sig.async_wait()) && conn->async_exec(req));
signal_set sig_set{ex, SIGINT, SIGTERM};
co_await sig_set.async_wait();
conn->cancel();
stream->cancel();
}
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
auto co_main(std::string host, std::string port) -> net::awaitable<void>
auto co_main(config const&) -> net::awaitable<void>
{
std::cout << "Requires support for posix streams." << std::endl;
co_return;

View File

@@ -4,19 +4,23 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#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>
#include "common/common.hpp"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using aedis::adapt;
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)
{
@@ -30,12 +34,6 @@ void print(std::vector<int> const& cont)
std::cout << "\n";
}
auto run(std::shared_ptr<connection> conn, std::string host, std::string port) -> net::awaitable<void>
{
co_await connect(conn, host, port);
co_await conn->async_run();
}
// Stores the content of some STL containers in Redis.
auto store(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
@@ -45,72 +43,60 @@ auto store(std::shared_ptr<connection> conn) -> net::awaitable<void>
std::map<std::string, std::string> map
{{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
resp3::request req;
req.push("HELLO", 3);
request req;
req.push_range("RPUSH", "rpush-key", vec);
req.push_range("HSET", "hset-key", map);
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore, net::deferred);
}
auto hgetall(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
// A request contains multiple commands.
resp3::request req;
req.push("HELLO", 3);
request req;
req.push("HGETALL", "hset-key");
// Responses as tuple elements.
std::tuple<aedis::ignore, std::map<std::string, std::string>> resp;
response<std::map<std::string, std::string>> resp;
// Executes the request and reads the response.
co_await conn->async_exec(req, adapt(resp));
co_await conn->async_exec(req, resp, net::deferred);
print(std::get<1>(resp));
print(std::get<0>(resp).value());
}
// Retrieves in a transaction.
auto transaction(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("HELLO", 3);
request req;
req.push("MULTI");
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
req.push("HGETALL", "hset-key"); // Retrieves
req.push("EXEC");
std::tuple<
aedis::ignore, // hello
aedis::ignore, // multi
aedis::ignore, // lrange
aedis::ignore, // hgetall
std::tuple<std::optional<std::vector<int>>, std::optional<std::map<std::string, std::string>>> // 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, adapt(resp));
co_await conn->async_exec(req, resp, net::deferred);
print(std::get<0>(std::get<4>(resp)).value());
print(std::get<1>(std::get<4>(resp)).value());
}
auto quit(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("QUIT");
co_await conn->async_exec(req);
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(std::string host, std::string port)
net::awaitable<void> co_main(config cfg)
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
net::co_spawn(ex, run(conn, host, port), net::detached);
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);
co_await quit(conn);
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -4,32 +4,38 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/co_spawn.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#include "common/common.hpp"
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using tcp_socket = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::socket>;
using tcp_acceptor = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::acceptor>;
using signal_set = net::use_awaitable_t<>::as_default_on_t<net::signal_set>;
using aedis::adapt;
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>;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
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>
{
resp3::request req;
std::string resp;
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");
req.push("PING", buffer);
auto tmp = std::tie(resp);
co_await conn->async_exec(req, adapt(tmp));
co_await net::async_write(socket, net::buffer(resp));
resp.clear();
co_await conn->async_exec(req, resp, net::deferred);
co_await net::async_write(socket, net::buffer(std::get<0>(resp).value()));
std::get<0>(resp).value().clear();
req.clear();
buffer.erase(0, n);
}
@@ -38,25 +44,27 @@ 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 ex = co_await net::this_coro::executor;
tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555});
for (;;)
net::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), net::detached);
try {
auto ex = co_await net::this_coro::executor;
tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555});
for (;;)
net::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), net::detached);
} catch (std::exception const& e) {
std::clog << "Listener: " << e.what() << std::endl;
}
}
// Called from the main function (see main.cpp)
auto co_main(std::string host, std::string port) -> net::awaitable<void>
auto co_main(config cfg) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
signal_set sig{ex, SIGINT, SIGTERM};
net::co_spawn(ex, listener(conn), net::detached);
conn->async_run(cfg, {}, net::consign(net::detached, conn));
resp3::request req;
req.push("HELLO", 3);
co_await connect(conn, host, port);
co_await ((conn->async_run() || listener(conn) || healthy_checker(conn) ||
sig.async_wait()) && conn->async_exec(req));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -4,58 +4,39 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#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 <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <aedis.hpp>
#include "common/common.hpp"
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using aedis::adapt;
using aedis::operation;
auto run(std::shared_ptr<connection> conn, std::string host, std::string port) -> net::awaitable<void>
{
co_await connect(conn, host, port);
co_await conn->async_run();
}
auto hello(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("HELLO", 3);
co_await conn->async_exec(req);
}
auto ping(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("PING", "Hello world");
std::tuple<std::string> resp;
co_await conn->async_exec(req, adapt(resp));
std::cout << "PING: " << std::get<0>(resp) << std::endl;
}
auto quit(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("QUIT");
co_await conn->async_exec(req);
}
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(std::string host, std::string port) -> net::awaitable<void>
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, run(conn, host, port), net::detached);
co_await hello(conn);
co_await ping(conn);
co_await quit(conn);
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 where the PONG response will be stored.
response<std::string> resp;
// Executes the request.
co_await conn->async_exec(req, resp, net::deferred);
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,35 +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/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#include "common/common.hpp"
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using aedis::adapt;
// Called from the main function (see main.cpp)
auto co_main(std::string host, std::string port) -> net::awaitable<void>
{
resp3::request req;
req.push("HELLO", 3);
req.push("PING", "Hello world");
req.push("QUIT");
std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
co_await connect(conn, host, port);
co_await (conn->async_run() || conn->async_exec(req, adapt(resp)));
std::cout << "PING: " << std::get<1>(resp) << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -4,24 +4,21 @@
* accompanying file LICENSE.txt)
*/
#include <tuple>
#include <string>
#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 <iostream>
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <boost/asio/ssl.hpp>
#include <aedis.hpp>
#include <aedis/ssl/connection.hpp>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using resolver = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::resolver>;
using aedis::adapt;
using connection = net::use_awaitable_t<>::as_default_on_t<aedis::ssl::connection>;
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
{
@@ -29,30 +26,29 @@ auto verify_certificate(bool, net::ssl::verify_context&) -> bool
return true;
}
net::awaitable<void> co_main(std::string, std::string)
auto co_main(config cfg) -> net::awaitable<void>
{
resp3::request req;
req.push("HELLO", 3, "AUTH", "aedis", "aedis");
cfg.use_ssl = true;
cfg.username = "aedis";
cfg.password = "aedis";
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));
request req;
req.push("PING");
req.push("QUIT");
std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
response<std::string> resp;
// Resolve
auto ex = co_await net::this_coro::executor;
resolver resv{ex};
auto const endpoints = co_await resv.async_resolve("db.occase.de", "6380");
conn->next_layer().set_verify_mode(net::ssl::verify_peer);
conn->next_layer().set_verify_callback(verify_certificate);
net::ssl::context ctx{net::ssl::context::sslv23};
connection conn{ex, ctx};
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);
conn->cancel();
co_await net::async_connect(conn.lowest_layer(), endpoints);
co_await conn.next_layer().async_handshake(net::ssl::stream_base::client);
co_await (conn.async_run() || conn.async_exec(req, adapt(resp)));
std::cout << "Response: " << std::get<1>(resp) << std::endl;
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

77
examples/cpp20_json.cpp Normal file
View File

@@ -0,0 +1,77 @@
/* 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/describe.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <string>
#include <iostream>
#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;
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;
// Struct that will be stored in Redis using json serialization.
struct user {
std::string name;
std::string age;
std::string country;
};
// The type must be described for serialization to work.
BOOST_DESCRIBE_STRUCT(user, (), (name, age, country))
// Boost.Redis customization points (examples/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;
auto conn = std::make_shared<connection>(ex);
conn->async_run(cfg, {}, net::consign(net::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.
response<ignore_t, user> resp;
co_await conn->async_exec(req, resp, net::deferred);
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";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,48 +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/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <aedis.hpp>
#include <string>
#include <iostream>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using resolver = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::resolver>;
using tcp_socket = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::socket>;
using aedis::adapter::adapt2;
using net::ip::tcp;
auto co_main(std::string host, std::string port) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
resolver resv{ex};
auto const addrs = co_await resv.async_resolve(host, port);
tcp_socket socket{ex};
co_await net::async_connect(socket, addrs);
// Creates the request and writes to the socket.
resp3::request req;
req.push("HELLO", 3);
req.push("PING", "Hello world");
req.push("QUIT");
co_await resp3::async_write(socket, req);
// Responses
std::string buffer, resp;
// Reads the responses to all commands in the request.
auto dbuffer = net::dynamic_buffer(buffer);
co_await resp3::async_read(socket, dbuffer);
co_await resp3::async_read(socket, dbuffer, adapt2(resp));
co_await resp3::async_read(socket, dbuffer);
std::cout << "Ping: " << resp << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,88 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/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/system/errc.hpp>
#include <iostream>
// See the definition in person.proto. This header is automatically
// generated by CMakeLists.txt.
#include "person.pb.h"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
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;
// The protobuf type described in examples/person.proto
using tutorial::person;
// Boost.Redis customization points (examples/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
// error codes.
void boost_redis_to_bulk(std::string& to, person const& u)
{
std::string tmp;
if (!u.SerializeToString(&tmp))
throw boost::system::system_error(boost::redis::error::invalid_data_type);
boost::redis::resp3::boost_redis_to_bulk(to, tmp);
}
void boost_redis_from_bulk(person& u, std::string_view sv, boost::system::error_code& ec)
{
std::string const tmp {sv};
if (!u.ParseFromString(tmp))
ec = boost::redis::error::invalid_data_type;
}
} // tutorial
using tutorial::boost_redis_to_bulk;
using tutorial::boost_redis_from_bulk;
net::awaitable<void> co_main(config cfg)
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
conn->async_run(cfg, {}, net::consign(net::detached, conn));
person p;
p.set_name("Louis");
p.set_id(3);
p.set_email("No email yet.");
request req;
req.push("SET", "protobuf-key", p);
req.push("GET", "protobuf-key");
response<ignore_t, person> resp;
// Sends the request and receives the response.
co_await conn->async_exec(req, resp, net::deferred);
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";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -4,62 +4,67 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#include <boost/redis/connection.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#include "common/common.hpp"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using endpoints = net::ip::tcp::resolver::results_type;
using aedis::adapt;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::address;
using boost::redis::connection;
auto redir(boost::system::error_code& ec)
{ return net::redirect_error(net::use_awaitable, ec); }
struct address {
std::string host;
std::string port;
};
// 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& endpoints) -> net::awaitable<address>
auto resolve_master_address(std::vector<address> const& addresses) -> net::awaitable<address>
{
resp3::request req;
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);
std::tuple<std::optional<std::array<std::string, 2>>, aedis::ignore> addr;
for (auto ep : endpoints) {
response<std::optional<std::array<std::string, 2>>, ignore_t> resp;
for (auto addr : addresses) {
boost::system::error_code ec;
co_await connect(conn, ep.host, ep.port);
co_await (conn->async_run() && conn->async_exec(req, adapt(addr), redir(ec)));
config cfg;
cfg.addr = addr;
// TODO: async_run and async_exec should be lauched in
// parallel here so we can wait for async_run completion
// before eventually calling it again.
conn->async_run(cfg, {}, net::consign(net::detached, conn));
co_await conn->async_exec(req, resp, redir(ec));
conn->cancel();
conn->reset_stream();
if (std::get<0>(addr))
co_return address{std::get<0>(addr).value().at(0), std::get<0>(addr).value().at(1)};
if (!ec && std::get<0>(resp))
co_return address{std::get<0>(resp).value().value().at(0), std::get<0>(resp).value().value().at(1)};
}
co_return address{};
}
auto co_main(std::string host, std::string port) -> net::awaitable<void>
auto co_main(config cfg) -> net::awaitable<void>
{
// A list of sentinel addresses from which only one is responsive.
// This simulates sentinels that are down.
std::vector<address> const endpoints
{ {"foo", "26379"}
, {"bar", "26379"}
, {host, port}
std::vector<address> const addresses
{ address{"foo", "26379"}
, address{"bar", "26379"}
, cfg.addr
};
auto const ep = co_await resolve_master_address(endpoints);
auto const ep = co_await resolve_master_address(addresses);
std::clog
<< "Host: " << ep.host << "\n"

View File

@@ -1,111 +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/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#define BOOST_JSON_NO_LIB
#define BOOST_CONTAINER_NO_LIB
#include <boost/json.hpp>
#include <aedis.hpp>
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <set>
#include <iterator>
#include <string>
#include "common/common.hpp"
// Include this in no more than one .cpp file.
#include <boost/json/src.hpp>
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using namespace boost::json;
using aedis::adapt;
struct user {
std::string name;
std::string age;
std::string country;
friend auto operator<(user const& a, user const& b)
{
return std::tie(a.name, a.age, a.country) < std::tie(b.name, b.age, b.country);
}
friend auto operator<<(std::ostream& os, user const& u) -> std::ostream&
{
os << "Name: " << u.name << "\n"
<< "Age: " << u.age << "\n"
<< "Country: " << u.country;
return os;
}
};
// Boost.Json serialization.
void tag_invoke(value_from_tag, value& jv, user const& u)
{
jv =
{ {"name", u.name}
, {"age", u.age}
, {"country", u.country}
};
}
template<class T>
void extract(object const& obj, T& t, std::string_view key)
{
t = value_to<T>(obj.at(key));
}
auto tag_invoke(value_to_tag<user>, value const& jv)
{
user u;
object const& obj = jv.as_object();
extract(obj, u.name, "name");
extract(obj, u.age, "age");
extract(obj, u.country, "country");
return u;
}
// Aedis serialization
void to_bulk(std::pmr::string& to, user const& u)
{
aedis::resp3::to_bulk(to, serialize(value_from(u)));
}
void from_bulk(user& u, std::string_view sv, boost::system::error_code&)
{
value jv = parse(sv);
u = value_to<user>(jv);
}
net::awaitable<void> co_main(std::string host, std::string port)
{
std::set<user> users
{{"Joao", "58", "Brazil"} , {"Serge", "60", "France"}};
resp3::request req;
req.push("HELLO", 3);
req.push_range("SADD", "sadd-key", users); // Sends
req.push("SMEMBERS", "sadd-key"); // Retrieves
req.push("QUIT");
std::tuple<aedis::ignore, int, std::set<user>, std::string> resp;
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
co_await connect(conn, host, port);
co_await (conn->async_run() || conn->async_exec(req, adapt(resp)));
for (auto const& e: std::get<2>(resp))
std::cout << e << "\n";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,98 @@
/* 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)

View File

@@ -4,18 +4,30 @@
* accompanying file LICENSE.txt)
*/
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <aedis.hpp>
#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/redirect_error.hpp>
#include <boost/asio/signal_set.hpp>
#include <iostream>
#include "common/common.hpp"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators;
using steady_timer = net::use_awaitable_t<>::as_default_on_t<net::steady_timer>;
using aedis::adapt;
using namespace std::chrono_literals;
using boost::redis::request;
using boost::redis::generic_response;
using boost::redis::logger;
using boost::redis::config;
using boost::redis::ignore;
using boost::system::error_code;
using boost::redis::connection;
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
/* This example will subscribe and read pushes indefinitely.
*
@@ -33,36 +45,46 @@ using aedis::adapt;
* > CLIENT kill TYPE pubsub
*/
// Receives pushes.
auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
// Receives server pushes.
auto
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
using resp_type = std::vector<resp3::node<std::string>>;
for (resp_type resp;;) {
co_await conn->async_receive(adapt(resp));
std::cout << resp.at(1).value << " " << resp.at(2).value << " " << resp.at(3).value << std::endl;
resp.clear();
request req;
req.push("SUBSCRIBE", "channel");
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req, ignore, net::deferred);
// 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));
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();
}
}
}
auto co_main(std::string host, std::string port) -> net::awaitable<void>
auto co_main(config cfg) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
steady_timer timer{ex};
net::co_spawn(ex, receiver(conn), net::detached);
conn->async_run(cfg, {}, net::consign(net::detached, conn));
resp3::request req;
req.push("HELLO", 3);
req.push("SUBSCRIBE", "channel");
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
// The loop will reconnect on connection lost. To exit type Ctrl-C twice.
for (;;) {
co_await connect(conn, host, port);
co_await ((conn->async_run() || healthy_checker(conn) || receiver(conn)) && conn->async_exec(req));
conn->reset_stream();
timer.expires_after(std::chrono::seconds{1});
co_await timer.async_wait();
}
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

53
examples/main.cpp Normal file
View File

@@ -0,0 +1,53 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/config.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <iostream>
namespace net = boost::asio;
using boost::redis::config;
using boost::redis::logger;
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
extern net::awaitable<void> co_main(config);
auto main(int argc, char * argv[]) -> int
{
try {
config cfg;
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
net::io_context ioc;
net::co_spawn(ioc, std::move(co_main(cfg)), [](std::exception_ptr p) {
if (p)
std::rethrow_exception(p);
});
ioc.run();
} catch (std::exception const& e) {
std::cerr << "(main) " << e.what() << std::endl;
return 1;
}
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int
{
std::cout << "Requires coroutine support." << std::endl;
return 0;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

9
examples/person.proto Normal file
View File

@@ -0,0 +1,9 @@
syntax = "proto2";
package tutorial;
message person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}

View File

@@ -0,0 +1,63 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_future.hpp>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
namespace boost::redis
{
class sync_connection {
public:
sync_connection()
: ioc_{1}
, conn_{std::make_shared<connection>(ioc_)}
{ }
~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);
ioc_.run();
}};
}
void stop()
{
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();
}
private:
asio::io_context ioc_{1};
std::shared_ptr<connection> conn_;
std::thread thread_;
};
}

View File

@@ -1,227 +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 AEDIS_ADAPT_HPP
#define AEDIS_ADAPT_HPP
#include <aedis/resp3/node.hpp>
#include <aedis/adapter/adapt.hpp>
#include <aedis/adapter/detail/response_traits.hpp>
#include <boost/mp11.hpp>
#include <boost/system.hpp>
#include <tuple>
#include <limits>
#include <string_view>
#include <variant>
namespace aedis {
/** @brief Tag used to ignore responses.
* @ingroup high-level-api
*
* For example
*
* @code
* std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
* @endcode
*
* will cause only the second tuple type to be parsed, the others
* will be ignored.
*/
using ignore = adapter::detail::ignore;
namespace detail
{
class ignore_adapter {
public:
explicit ignore_adapter(std::size_t max_read_size) : max_read_size_{max_read_size} {}
void
operator()(
std::size_t, resp3::node<std::string_view> const&, boost::system::error_code&) { }
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return static_cast<std::size_t>(-1);}
[[nodiscard]]
auto get_max_read_size(std::size_t) const noexcept
{ return max_read_size_;}
private:
std::size_t max_read_size_;
};
template <class Tuple>
class static_adapter {
private:
static constexpr auto size = std::tuple_size<Tuple>::value;
using adapter_tuple = boost::mp11::mp_transform<adapter::adapter_t, Tuple>;
using variant_type = boost::mp11::mp_rename<adapter_tuple, std::variant>;
using adapters_array_type = std::array<variant_type, size>;
adapters_array_type adapters_;
std::size_t max_read_size_;
public:
explicit static_adapter(Tuple& r, std::size_t max_read_size)
: max_read_size_{max_read_size}
{
adapter::detail::assigner<size - 1>::assign(adapters_, r);
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return size;}
[[nodiscard]]
auto get_max_read_size(std::size_t) const noexcept
{ return max_read_size_;}
void
operator()(
std::size_t i,
resp3::node<std::string_view> const& nd,
boost::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(nd, ec);}, adapters_.at(i));
}
};
template <class Vector>
class vector_adapter {
private:
using adapter_type = typename adapter::detail::response_traits<Vector>::adapter_type;
adapter_type adapter_;
std::size_t max_read_size_;
public:
explicit vector_adapter(Vector& v, std::size_t max_read_size)
: adapter_{adapter::adapt2(v)}
, max_read_size_{max_read_size}
{ }
[[nodiscard]]
auto
get_supported_response_size() const noexcept
{ return static_cast<std::size_t>(-1);}
[[nodiscard]]
auto get_max_read_size(std::size_t) const noexcept
{ return max_read_size_;}
void
operator()(
std::size_t,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
{
adapter_(nd, ec);
}
};
template <class>
struct response_traits;
template <>
struct response_traits<void> {
using response_type = void;
using adapter_type = detail::ignore_adapter;
static auto adapt(std::size_t max_read_size) noexcept
{ return detail::ignore_adapter{max_read_size}; }
};
template <class String, class Allocator>
struct response_traits<std::vector<resp3::node<String>, Allocator>> {
using response_type = std::vector<resp3::node<String>, Allocator>;
using adapter_type = vector_adapter<response_type>;
static auto adapt(response_type& v, std::size_t max_read_size) noexcept
{ return adapter_type{v, max_read_size}; }
};
template <class ...Ts>
struct response_traits<std::tuple<Ts...>> {
using response_type = std::tuple<Ts...>;
using adapter_type = static_adapter<response_type>;
static auto adapt(response_type& r, std::size_t max_read_size) noexcept
{ return adapter_type{r, max_read_size}; }
};
template <class Adapter>
class wrapper {
public:
explicit wrapper(Adapter adapter) : adapter_{adapter} {}
void operator()(resp3::node<std::string_view> const& node, boost::system::error_code& ec)
{ return adapter_(0, node, ec); }
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return adapter_.get_supported_response_size();}
[[nodiscard]]
auto get_max_read_size(std::size_t) const noexcept
{ return adapter_.get_max_read_size(0); }
private:
Adapter adapter_;
};
template <class Adapter>
auto make_adapter_wrapper(Adapter adapter)
{
return wrapper{adapter};
}
} // detail
/** @brief Creates an adapter that ignores responses.
* @ingroup high-level-api
*
* This function can be used to create adapters that ignores
* responses.
*
* @param max_read_size Specifies the maximum size of the read
* buffer.
*/
inline auto adapt(std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)()) noexcept
{
return detail::response_traits<void>::adapt(max_read_size);
}
/** @brief Adapts a type to be used as a response.
* @ingroup high-level-api
*
* The type T must be either
*
* 1. a std::tuple<T1, T2, T3, ...> or
* 2. 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.
* @param max_read_size Specifies the maximum size of the read
* buffer.
*/
template<class T>
auto adapt(T& t, std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)()) noexcept
{
return detail::response_traits<T>::adapt(t, max_read_size);
}
} // aedis
#endif // AEDIS_ADAPT_HPP

View File

@@ -1,80 +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 AEDIS_ADAPTER_ADAPT_HPP
#define AEDIS_ADAPTER_ADAPT_HPP
#include <aedis/adapter/detail/response_traits.hpp>
namespace aedis::adapter {
template <class T>
using adapter_t = typename detail::adapter_t<T>;
/** \brief Creates a dummy response adapter.
\ingroup low-level-api
The adapter returned by this function ignores responses. It is
useful to avoid wasting time with responses which are not needed.
Example:
@code
// Pushes and writes some commands to the server.
sr.push(command::hello, 3);
sr.push(command::ping);
sr.push(command::quit);
net::write(socket, net::buffer(request));
// Ignores all responses except for the response to ping.
std::string buffer;
resp3::read(socket, dynamic_buffer(buffer), adapt()); // hello
resp3::read(socket, dynamic_buffer(buffer), adapt(resp)); // ping
resp3::read(socket, dynamic_buffer(buffer, adapt())); // quit
@endcode
*/
inline
auto adapt2() noexcept
{ return detail::response_traits<void>::adapt(); }
/** \brief Adapts user data to read operations.
* \ingroup low-level-api
*
* STL containers, \c std::tuple 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
* std::tuple<std::string, int, int, std::vector<std::string>, int> execs;
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs));
* @endcode
*/
template<class T>
auto adapt2(T& t) noexcept
{ return detail::response_traits<T>::adapt(t); }
} // aedis::adapter
#endif // AEDIS_ADAPTER_ADAPT_HPP

View File

@@ -1,214 +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 AEDIS_CONNECTION_HPP
#define AEDIS_CONNECTION_HPP
#include <aedis/detail/connection_base.hpp>
#include <boost/asio/io_context.hpp>
#include <chrono>
#include <memory>
namespace aedis {
/** @brief A connection to the Redis server.
* @ingroup high-level-api
*
* For more details, please see the documentation of each individual
* function.
*
* @tparam AsyncReadWriteStream A stream that supports reading and
* writing.
*/
template <class AsyncReadWriteStream>
class basic_connection :
private detail::connection_base<
typename AsyncReadWriteStream::executor_type,
basic_connection<AsyncReadWriteStream>> {
public:
/// Executor type.
using executor_type = typename AsyncReadWriteStream::executor_type;
/// Type of the next layer
using next_layer_type = AsyncReadWriteStream;
/// Rebinds the socket type to another executor.
template <class Executor1>
struct rebind_executor
{
/// The socket type when rebound to the specified executor.
using other = basic_connection<typename next_layer_type::template rebind_executor<Executor1>::other>;
};
using base_type = detail::connection_base<executor_type, basic_connection<AsyncReadWriteStream>>;
/// Contructs from an executor.
explicit
basic_connection(
executor_type ex,
std::pmr::memory_resource* resource = std::pmr::get_default_resource())
: base_type{ex, resource}
, stream_{ex}
{}
/// Contructs from a context.
explicit
basic_connection(
boost::asio::io_context& ioc,
std::pmr::memory_resource* resource = std::pmr::get_default_resource())
: basic_connection(ioc.get_executor(), resource)
{ }
/// Returns the associated executor.
auto get_executor() {return stream_.get_executor();}
/// Resets the underlying stream.
void reset_stream()
{
if (stream_.is_open()) {
boost::system::error_code ignore;
stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignore);
stream_.close(ignore);
}
}
/// Returns a reference to the next layer.
auto next_layer() noexcept -> auto& { return stream_; }
/// Returns a const reference to the next layer.
auto next_layer() const noexcept -> auto const& { return stream_; }
/** @brief Starts read and write operations
*
* This function starts read and write operations with the Redis
* server. More specifically it will trigger the write of all
* requests i.e. calls to `async_exec` that happened prior to this
* call.
*
* @param token Completion token.
*
* The completion token must have the following signature
*
* @code
* void f(boost::system::error_code);
* @endcode
*
* This function will complete when the connection is lost. If the
* error is boost::asio::error::eof this function will complete
* without error.
*/
template <class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_run(CompletionToken token = CompletionToken{})
{
return base_type::async_run(std::move(token));
}
/** @brief Executes a command on the Redis server asynchronously.
*
* This function sends a request to the Redis server and
* complete after the response has been processed. If the request
* contains only commands that don't expect a response, the
* completion occurs after it has been written to the underlying
* stream. Multiple concurrent calls to this function will be
* automatically queued by the implementation.
*
* @param req Request object.
* @param adapter Response adapter.
* @param token Asio completion token.
*
* For an example see cpp20_echo_server.cpp. The completion token must
* have the following signature
*
* @code
* void f(boost::system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the response in
* bytes.
*/
template <
class Adapter = detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_exec(
resp3::request const& req,
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
return base_type::async_exec(req, adapter, std::move(token));
}
/** @brief Receives server side pushes asynchronously.
*
* Users that expect server pushes should call this function in a
* loop. If a push arrives and there is no reader, the connection
* will hang.
*
* @param adapter The response adapter.
* @param token The Asio completion token.
*
* For an example see cpp20_subscriber.cpp. The completion token must
* have the following signature
*
* @code
* void f(boost::system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the push in
* bytes.
*/
template <
class Adapter = detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_receive(
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
return base_type::async_receive(adapter, std::move(token));
}
/** @brief Cancel operations.
*
* @li `operation::exec`: Cancels operations started with
* `async_exec`. Affects only requests that haven't been written
* yet.
* @li operation::run: Cancels the `async_run` operation. Notice
* that the preferred way to close a connection is to send a
* [QUIT](https://redis.io/commands/quit/) command to the server.
* @li operation::receive: Cancels any ongoing calls to * `async_receive`.
*
* @param op: The operation to be cancelled.
* @returns The number of operations that have been canceled.
*/
auto cancel(operation op) -> std::size_t
{ return base_type::cancel(op); }
private:
using this_type = basic_connection<next_layer_type>;
template <class, class> friend class detail::connection_base;
template <class, class> friend struct detail::exec_read_op;
template <class, class> friend struct detail::exec_op;
template <class> friend struct detail::reader_op;
template <class> friend struct detail::writer_op;
template <class> friend struct detail::run_op;
void close() { stream_.close(); }
auto is_open() const noexcept { return stream_.is_open(); }
auto lowest_layer() noexcept -> auto& { return stream_.lowest_layer(); }
AsyncReadWriteStream stream_;
};
/** \brief A connection that uses a boost::asio::ip::tcp::socket.
* \ingroup high-level-api
*/
using connection = basic_connection<boost::asio::ip::tcp::socket>;
} // aedis
#endif // AEDIS_CONNECTION_HPP

View File

@@ -1,376 +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 AEDIS_CONNECTION_BASE_HPP
#define AEDIS_CONNECTION_BASE_HPP
#include <aedis/adapt.hpp>
#include <aedis/operation.hpp>
#include <aedis/resp3/request.hpp>
#include <aedis/detail/connection_ops.hpp>
#include <boost/assert.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/experimental/channel.hpp>
#include <vector>
#include <queue>
#include <limits>
#include <chrono>
#include <memory>
#include <type_traits>
#include <memory_resource>
namespace aedis::detail {
/** Base class for high level Redis asynchronous connections.
*
* This class is not meant to be instantiated directly but as base
* class in the CRTP.
*
* @tparam Executor The executor type.
* @tparam Derived The derived class type.
*
*/
template <class Executor, class Derived>
class connection_base {
public:
using executor_type = Executor;
using this_type = connection_base<Executor, Derived>;
connection_base(executor_type ex, std::pmr::memory_resource* resource)
: writer_timer_{ex}
, read_timer_{ex}
, guarded_op_{ex}
, read_buffer_{resource}
, write_buffer_{resource}
, reqs_{resource}
{
writer_timer_.expires_at(std::chrono::steady_clock::time_point::max());
read_timer_.expires_at(std::chrono::steady_clock::time_point::max());
}
auto get_executor() {return writer_timer_.get_executor();}
auto cancel(operation op) -> std::size_t
{
switch (op) {
case operation::exec:
{
return cancel_unwritten_requests();
}
case operation::run:
{
derived().close();
read_timer_.cancel();
writer_timer_.cancel();
cancel_on_conn_lost();
return 1U;
}
case operation::receive:
{
guarded_op_.cancel();
return 1U;
}
default: BOOST_ASSERT(false); return 0;
}
}
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;
}
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;
}
template <class Adapter, class CompletionToken>
auto async_exec(resp3::request const& req, Adapter adapter, CompletionToken token)
{
BOOST_ASSERT_MSG(req.size() <= adapter.get_supported_response_size(), "Request and adapter have incompatible sizes.");
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_op<Derived, Adapter>{&derived(), &req, adapter}, token, writer_timer_);
}
template <class Adapter, class CompletionToken>
auto async_receive(Adapter adapter, CompletionToken token)
{
auto f = detail::make_adapter_wrapper(adapter);
return guarded_op_.async_wait(
resp3::async_read(derived().next_layer(), make_dynamic_buffer(adapter.get_max_read_size(0)), f, boost::asio::deferred),
std::move(token));
}
template <class CompletionToken>
auto async_run(CompletionToken token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::run_op<Derived>{&derived()}, token, writer_timer_);
}
private:
using clock_type = std::chrono::steady_clock;
using clock_traits_type = boost::asio::wait_traits<clock_type>;
using timer_type = boost::asio::basic_waitable_timer<clock_type, clock_traits_type, executor_type>;
auto derived() -> Derived& { return static_cast<Derived&>(*this); }
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();
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
if (ptr->is_staged())
ptr->mark_written();
});
}
struct req_info {
public:
enum class action
{
stop,
proceed,
none,
};
explicit req_info(resp3::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_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_;
resp3::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::pmr::deque<std::shared_ptr<req_info>>;
template <class> friend struct detail::reader_op;
template <class> friend struct detail::writer_op;
template <class> friend struct detail::run_op;
template <class, class> friend struct detail::exec_op;
template <class, class> friend struct detail::exec_read_op;
template <class> friend struct detail::send_receive_op;
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_));
}
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_written() && !e->is_staged();
});
std::rotate(std::rbegin(reqs_), std::rbegin(reqs_) + 1, rend);
}
if (derived().is_open() && cmds_ == 0 && write_buffer_.empty())
writer_timer_.cancel();
}
auto make_dynamic_buffer(std::size_t max_read_size = 512)
{ return boost::asio::dynamic_buffer(read_buffer_, max_read_size); }
template <class CompletionToken>
auto reader(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::reader_op<Derived>{&derived()}, token, writer_timer_);
}
template <class CompletionToken>
auto writer(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::writer_op<Derived>{&derived()}, token, writer_timer_);
}
template <class Adapter, class CompletionToken>
auto async_exec_read(Adapter adapter, std::size_t cmds, CompletionToken token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_read_op<Derived, Adapter>{&derived(), adapter, cmds}, token, writer_timer_);
}
void stage_request(req_info& ri)
{
write_buffer_ += ri.get_request().payload();
cmds_ += ri.get_request().size();
ri.mark_staged();
}
void coalesce_requests()
{
// Coalesce the requests and marks them staged. After a
// successful write staged requests will be marked as written.
BOOST_ASSERT(write_buffer_.empty());
BOOST_ASSERT(!reqs_.empty());
stage_request(*reqs_.at(0));
for (std::size_t i = 1; i < std::size(reqs_); ++i) {
if (!reqs_.at(i - 1)->get_request().get_config().coalesce ||
!reqs_.at(i - 0)->get_request().get_config().coalesce) {
break;
}
stage_request(*reqs_.at(i));
}
}
// 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_;
detail::guarded_operation<executor_type> guarded_op_;
std::pmr::string read_buffer_;
std::pmr::string write_buffer_;
std::size_t cmds_ = 0;
reqs_type reqs_;
};
} // aedis
#endif // AEDIS_CONNECTION_BASE_HPP

View File

@@ -1,349 +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 AEDIS_CONNECTION_OPS_HPP
#define AEDIS_CONNECTION_OPS_HPP
#include <aedis/adapt.hpp>
#include <aedis/error.hpp>
#include <aedis/detail/guarded_operation.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/read.hpp>
#include <aedis/resp3/write.hpp>
#include <aedis/resp3/request.hpp>
#include <boost/assert.hpp>
#include <boost/system.hpp>
#include <boost/asio/write.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <array>
#include <algorithm>
#include <string_view>
namespace aedis::detail {
template <class Conn, class Adapter>
struct exec_read_op {
Conn* conn;
Adapter adapter;
std::size_t cmds = 0;
std::size_t read_size = 0;
std::size_t index = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
BOOST_ASIO_CORO_REENTER (coro)
{
// Loop reading the responses to this request.
BOOST_ASSERT(!conn->reqs_.empty());
while (cmds != 0) {
BOOST_ASSERT(conn->cmds_ != 0);
//-----------------------------------
// 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()) {
BOOST_ASIO_CORO_YIELD
boost::asio::async_read_until(
conn->next_layer(),
conn->make_dynamic_buffer(),
"\r\n", std::move(self));
AEDIS_CHECK_OP1(conn->cancel(operation::run););
}
// 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->guarded_op_.async_run(std::move(self));
AEDIS_CHECK_OP1(conn->cancel(operation::run););
continue;
}
//-----------------------------------
BOOST_ASIO_CORO_YIELD
resp3::async_read(
conn->next_layer(),
conn->make_dynamic_buffer(adapter.get_max_read_size(index)),
[i = index, adpt = adapter] (resp3::node<std::string_view> const& nd, boost::system::error_code& ec) mutable { adpt(i, nd, ec); },
std::move(self));
++index;
AEDIS_CHECK_OP1(conn->cancel(operation::run););
read_size += n;
BOOST_ASSERT(cmds != 0);
--cmds;
BOOST_ASSERT(conn->cmds_ != 0);
--conn->cmds_;
}
self.complete({}, read_size);
}
}
};
template <class Conn, class Adapter>
struct exec_op {
using req_info_type = typename Conn::req_info;
Conn* conn = nullptr;
resp3::request const* req = nullptr;
Adapter adapter{};
std::shared_ptr<req_info_type> info = nullptr;
std::size_t read_size = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::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()) {
return self.complete(error::not_connected, 0);
}
info = std::allocate_shared<req_info_type>(boost::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 == boost::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 = boost::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_ASSERT(conn->cmds_ != 0);
BOOST_ASIO_CORO_YIELD
conn->async_exec_read(adapter, conn->reqs_.front()->get_number_of_commands(), std::move(self));
AEDIS_CHECK_OP1(;);
read_size = n;
BOOST_ASSERT(!conn->reqs_.empty());
conn->reqs_.pop_front();
if (conn->cmds_ == 0) {
conn->read_timer_.cancel_one();
if (!conn->reqs_.empty())
conn->writer_timer_.cancel_one();
} else {
BOOST_ASSERT(!conn->reqs_.empty());
conn->reqs_.front()->proceed();
}
self.complete({}, read_size);
}
}
};
template <class Conn>
struct run_op {
Conn* conn = nullptr;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, boost::system::error_code ec0 = {}
, boost::system::error_code ec1 = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
conn->write_buffer_.clear();
conn->cmds_ = 0;
BOOST_ASIO_CORO_YIELD
boost::asio::experimental::make_parallel_group(
[this](auto token) { return conn->reader(token);},
[this](auto token) { return conn->writer(token);}
).async_wait(
boost::asio::experimental::wait_for_one(),
std::move(self));
if (is_cancelled(self)) {
self.complete(boost::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>
struct writer_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
boost::ignore_unused(n);
BOOST_ASIO_CORO_REENTER (coro) for (;;)
{
while (!conn->reqs_.empty() && conn->cmds_ == 0 && conn->write_buffer_.empty()) {
conn->coalesce_requests();
BOOST_ASIO_CORO_YIELD
boost::asio::async_write(conn->next_layer(), boost::asio::buffer(conn->write_buffer_), std::move(self));
AEDIS_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;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
boost::ignore_unused(n);
BOOST_ASIO_CORO_REENTER (coro) for (;;)
{
BOOST_ASIO_CORO_YIELD
boost::asio::async_read_until(
conn->next_layer(),
conn->make_dynamic_buffer(),
"\r\n", std::move(self));
if (ec == boost::asio::error::eof) {
conn->cancel(operation::run);
return self.complete({}); // EOFINAE: EOF is not an error.
}
AEDIS_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.
//
BOOST_ASSERT(!conn->read_buffer_.empty());
if (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push
|| conn->reqs_.empty()
|| (!conn->reqs_.empty() && conn->reqs_.front()->get_number_of_commands() == 0)) {
BOOST_ASIO_CORO_YIELD
conn->guarded_op_.async_run(std::move(self));
} else {
BOOST_ASSERT(conn->cmds_ != 0);
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(boost::asio::error::basic_errors::operation_aborted);
return;
}
}
}
};
} // aedis::detail
#endif // AEDIS_CONNECTION_OPS_HPP

View File

@@ -1,111 +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 AEDIS_DETAIL_GUARDED_OPERATION_HPP
#define AEDIS_DETAIL_GUARDED_OPERATION_HPP
#include <boost/asio/experimental/channel.hpp>
namespace aedis::detail {
template <class Executor>
struct send_receive_op {
using channel_type = boost::asio::experimental::channel<Executor, void(boost::system::error_code, std::size_t)>;
channel_type* channel;
boost::asio::coroutine coro{};
template <class Self>
void operator()(Self& self, boost::system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
BOOST_ASIO_CORO_YIELD
channel->async_send(boost::system::error_code{}, 0, std::move(self));
AEDIS_CHECK_OP0(;);
BOOST_ASIO_CORO_YIELD
channel->async_send(boost::system::error_code{}, 0, std::move(self));
AEDIS_CHECK_OP0(;);
self.complete({});
}
}
};
template <class Executor, class Op>
struct wait_op {
using channel_type = boost::asio::experimental::channel<Executor, void(boost::system::error_code, std::size_t)>;
channel_type* channel;
Op op;
std::size_t res = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
BOOST_ASIO_CORO_REENTER (coro)
{
BOOST_ASIO_CORO_YIELD
channel->async_receive(std::move(self));
AEDIS_CHECK_OP1(;);
BOOST_ASIO_CORO_YIELD
std::move(op)(std::move(self));
AEDIS_CHECK_OP1(channel->cancel(););
res = n;
BOOST_ASIO_CORO_YIELD
channel->async_receive(std::move(self));
AEDIS_CHECK_OP1(;);
self.complete({}, res);
return;
}
}
};
template <class Executor>
class guarded_operation {
public:
using executor_type = Executor;
guarded_operation(executor_type ex) : channel_{ex} {}
template <class CompletionToken>
auto async_run(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(send_receive_op<executor_type>{&channel_}, token, channel_);
}
template <class Op, class CompletionToken>
auto async_wait(Op&& op, CompletionToken token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(wait_op<executor_type, Op>{&channel_, std::move(op)}, token, channel_);
}
void cancel() {channel_.cancel();}
private:
using channel_type = boost::asio::experimental::channel<executor_type, void(boost::system::error_code, std::size_t)>;
template <class> friend struct send_receive_op;
template <class, class> friend struct wait_op;
channel_type channel_;
};
} // aedis::detail
#endif // AEDIS_DETAIL_GUARDED_OPERATION_HPP

View File

@@ -1,29 +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 AEDIS_OPERATION_HPP
#define AEDIS_OPERATION_HPP
namespace aedis {
/** \brief Connection operations that can be cancelled.
* \ingroup high-level-api
*
* The operations listed below can be passed to the
* `aedis::connection::cancel` member function.
*/
enum class operation {
/// Refers to `connection::async_exec` operations.
exec,
/// Refers to `connection::async_run` operations.
run,
/// Refers to `connection::async_receive` operations.
receive,
};
} // aedis
#endif // AEDIS_OPERATION_HPP

View File

@@ -1,156 +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 <aedis/resp3/detail/parser.hpp>
#include <aedis/error.hpp>
#include <boost/assert.hpp>
#include <charconv>
namespace aedis::resp3::detail {
void to_int(int_type& i, std::string_view sv, boost::system::error_code& ec)
{
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), i);
if (res.ec != std::errc())
ec = error::not_a_number;
}
parser::parser()
{
sizes_[0] = 2; // The sentinel must be more than 1.
}
auto
parser::consume(
char const* data,
std::size_t n,
boost::system::error_code& ec) -> std::pair<node_type, std::size_t>
{
node_type ret;
if (bulk_expected()) {
n = bulk_length_ + 2;
ret = {bulk_, 1, depth_, {data, bulk_length_}};
bulk_ = type::invalid;
--sizes_[depth_];
} else if (sizes_[depth_] != 0) {
auto const t = to_type(*data);
switch (t) {
case type::streamed_string_part:
{
to_int(bulk_length_ , std::string_view{data + 1, n - 3}, ec);
if (ec)
return std::make_pair(node_type{}, 0);
if (bulk_length_ == 0) {
ret = {type::streamed_string_part, 1, depth_, {}};
sizes_[depth_] = 0; // We are done.
bulk_ = type::invalid;
} else {
bulk_ = type::streamed_string_part;
}
} break;
case type::blob_error:
case type::verbatim_string:
case type::blob_string:
{
if (data[1] == '?') {
// NOTE: This can only be triggered with blob_string.
// Trick: A streamed string is read as an aggregate
// of infinite lenght. When the streaming is done
// the server is supposed to send a part with length
// 0.
sizes_[++depth_] = (std::numeric_limits<std::size_t>::max)();
ret = {type::streamed_string, 0, depth_, {}};
} else {
to_int(bulk_length_ , std::string_view{data + 1, n - 3} , ec);
if (ec)
return std::make_pair(node_type{}, 0);
bulk_ = t;
}
} break;
case type::boolean:
{
if (n == 3) {
ec = error::empty_field;
return std::make_pair(node_type{}, 0);
}
if (data[1] != 'f' && data[1] != 't') {
ec = error::unexpected_bool_value;
return std::make_pair(node_type{}, 0);
}
ret = {t, 1, depth_, {data + 1, n - 3}};
--sizes_[depth_];
} break;
case type::doublean:
case type::big_number:
case type::number:
{
if (n == 3) {
ec = error::empty_field;
return std::make_pair(node_type{}, 0);
}
ret = {t, 1, depth_, {data + 1, n - 3}};
--sizes_[depth_];
} break;
case type::simple_error:
case type::simple_string:
{
ret = {t, 1, depth_, {&data[1], n - 3}};
--sizes_[depth_];
} break;
case type::null:
{
ret = {type::null, 1, depth_, {}};
--sizes_[depth_];
} break;
case type::push:
case type::set:
case type::array:
case type::attribute:
case type::map:
{
int_type l = -1;
to_int(l, std::string_view{data + 1, n - 3}, ec);
if (ec)
return std::make_pair(node_type{}, 0);
ret = {t, l, depth_, {}};
if (l == 0) {
--sizes_[depth_];
} else {
if (depth_ == max_embedded_depth) {
ec = error::exceeeds_max_nested_depth;
return std::make_pair(node_type{}, 0);
}
++depth_;
sizes_[depth_] = l * element_multiplicity(t);
}
} break;
default:
{
ec = error::invalid_data_type;
return std::make_pair(node_type{}, 0);
}
}
}
while (sizes_[depth_] == 0) {
--depth_;
--sizes_[depth_];
}
return std::make_pair(ret, n);
}
} // aedis::resp3::detail

View File

@@ -1,69 +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 AEDIS_RESP3_PARSER_HPP
#define AEDIS_RESP3_PARSER_HPP
#include <aedis/resp3/node.hpp>
#include <array>
#include <limits>
#include <string_view>
#include <cstdint>
namespace aedis::resp3::detail {
using int_type = std::uint64_t;
class parser {
private:
using node_type = node<std::string_view>;
static constexpr std::size_t max_embedded_depth = 5;
// The current depth. Simple data types will have depth 0, whereas
// the elements of aggregates will have depth 1. Embedded types
// will have increasing depth.
std::size_t depth_ = 0;
// The parser supports up to 5 levels of nested structures. The
// first element in the sizes stack is a sentinel and must be
// different from 1.
std::array<std::size_t, max_embedded_depth + 1> sizes_ = {{1}};
// Contains the length expected in the next bulk read.
int_type bulk_length_ = (std::numeric_limits<unsigned long>::max)();
// The type of the next bulk. Contains type::invalid if no bulk is
// expected.
type bulk_ = type::invalid;
public:
parser();
// Returns the number of bytes that have been consumed.
auto
consume(
char const* data,
std::size_t n,
boost::system::error_code& ec) -> std::pair<node_type, std::size_t>;
// Returns true when the parser is done with the current message.
[[nodiscard]] auto done() const noexcept
{ return depth_ == 0 && bulk_ == type::invalid; }
// The bulk type expected in the next read. If none is expected
// returns type::invalid.
[[nodiscard]] auto bulk_expected() const noexcept -> bool
{ return bulk_ != type::invalid; }
// The length expected in the the next bulk.
[[nodiscard]] auto bulk_length() const noexcept
{ return bulk_length_; }
};
} // detail::resp3::aedis
#endif // AEDIS_RESP3_PARSER_HPP

View File

@@ -1,134 +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 AEDIS_RESP3_READ_OPS_HPP
#define AEDIS_RESP3_READ_OPS_HPP
#include <aedis/resp3/detail/parser.hpp>
#include <boost/assert.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/core/ignore_unused.hpp>
#include <string_view>
namespace aedis::detail
{
template <class T>
auto is_cancelled(T const& self)
{
return self.get_cancellation_state().cancelled() != boost::asio::cancellation_type_t::none;
}
}
#define AEDIS_CHECK_OP0(X)\
if (ec || aedis::detail::is_cancelled(self)) {\
X\
self.complete(!!ec ? ec : boost::asio::error::operation_aborted);\
return;\
}
#define AEDIS_CHECK_OP1(X)\
if (ec || aedis::detail::is_cancelled(self)) {\
X\
self.complete(!!ec ? ec : boost::asio::error::operation_aborted, {});\
return;\
}
namespace aedis::resp3::detail {
struct ignore_response {
void operator()(node<std::string_view> nd, boost::system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = error::resp3_simple_error; return;
case resp3::type::blob_error: ec = error::resp3_blob_error; return;
default: return;
}
}
};
template <
class AsyncReadStream,
class DynamicBuffer,
class ResponseAdapter>
class parse_op {
private:
AsyncReadStream& stream_;
DynamicBuffer buf_;
parser parser_;
ResponseAdapter adapter_;
std::size_t consumed_ = 0;
std::size_t buffer_size_ = 0;
boost::asio::coroutine coro_{};
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
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;) {
if (!parser_.bulk_expected()) {
BOOST_ASIO_CORO_YIELD
boost::asio::async_read_until(stream_, buf_, "\r\n", std::move(self));
AEDIS_CHECK_OP1(;);
} else {
// On a bulk read we can't read until delimiter since the
// payload may contain the delimiter itself so we have to
// read the whole chunk. However if the bulk blob is small
// enough it may be already on the buffer (from the last
// read), in which case there is no need of initiating
// another async op, otherwise we have to read the missing
// bytes.
if (buf_.size() < (parser_.bulk_length() + 2)) {
buffer_size_ = buf_.size();
buf_.grow(parser_.bulk_length() + 2 - buffer_size_);
BOOST_ASIO_CORO_YIELD
boost::asio::async_read(
stream_,
buf_.data(buffer_size_, parser_.bulk_length() + 2 - buffer_size_),
boost::asio::transfer_all(),
std::move(self));
AEDIS_CHECK_OP1(;);
}
n = parser_.bulk_length() + 2;
BOOST_ASSERT(buf_.size() >= n);
}
auto const res = parser_.consume(static_cast<char const*>(buf_.data(0, n).data()), n, ec);
if (ec)
return self.complete(ec, 0);
if (!parser_.bulk_expected()) {
adapter_(res.first, ec);
if (ec)
return self.complete(ec, 0);
}
buf_.consume(res.second);
consumed_ += res.second;
if (parser_.done()) {
self.complete({}, consumed_);
return;
}
}
}
};
} // aedis::resp3::detail
#endif // AEDIS_RESP3_READ_OPS_HPP

View File

@@ -1,186 +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 AEDIS_RESP3_READ_HPP
#define AEDIS_RESP3_READ_HPP
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/detail/read_ops.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/async_result.hpp>
namespace aedis::resp3 {
/** \brief Reads a complete response to a command sychronously.
* \ingroup low-level-api
*
* 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,
boost::system::error_code& ec) -> std::size_t
{
detail::parser p;
std::size_t n = 0;
std::size_t consumed = 0;
do {
if (!p.bulk_expected()) {
n = boost::asio::read_until(stream, buf, "\r\n", ec);
if (ec)
return 0;
} else {
auto const s = buf.size();
auto const l = p.bulk_length();
if (s < (l + 2)) {
auto const to_read = l + 2 - s;
buf.grow(to_read);
n = boost::asio::read(stream, buf.data(s, to_read), ec);
if (ec)
return 0;
}
}
auto const* data = static_cast<char const*>(buf.data(0, n).data());
auto const res = p.consume(data, n, ec);
if (ec)
return 0;
if (!p.bulk_expected()) {
adapter(res.first, ec);
if (ec)
return 0;
}
buf.consume(res.second);
consumed += res.second;
} while (!p.done());
return consumed;
}
/** \brief Reads a complete response to a command sychronously.
* \ingroup low-level-api
*
* Same as the error_code overload but throws on error.
*/
template<
class SyncReadStream,
class DynamicBuffer,
class ResponseAdapter = detail::ignore_response>
auto
read(
SyncReadStream& stream,
DynamicBuffer buf,
ResponseAdapter adapter = ResponseAdapter{})
{
boost::system::error_code ec;
auto const n = resp3::read(stream, buf, adapter, ec);
if (ec)
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
return n;
}
/** \brief Reads a complete response to a Redis command asynchronously.
* \ingroup low-level-api
*
* 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(boost::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 = detail::ignore_response,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>
>
auto async_read(
AsyncReadStream& stream,
DynamicBuffer buffer,
ResponseAdapter adapter = ResponseAdapter{},
CompletionToken&& token =
boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::parse_op<AsyncReadStream, DynamicBuffer, ResponseAdapter> {stream, buffer, adapter},
token,
stream);
}
} // aedis::resp3
#endif // AEDIS_RESP3_READ_HPP

View File

@@ -1,405 +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 AEDIS_RESP3_REQUEST_HPP
#define AEDIS_RESP3_REQUEST_HPP
#include <aedis/resp3/type.hpp>
#include <string>
#include <tuple>
#include <memory_resource>
// NOTE: Consider detecting tuples in the type in the parameter pack
// to calculate the header size correctly.
//
// NOTE: For some commands like hset it would be a good idea to assert
// the value type is a pair.
namespace aedis::resp3 {
constexpr char const* separator = "\r\n";
/** @brief Adds a bulk to the request.
* @relates request
*
* This function is useful in serialization of your own data
* structures in a request. For example
*
* @code
* void to_bulk(std::string& to, mystruct const& obj)
* {
* auto const str = // Convert obj to a string.
* resp3::to_bulk(to, str);
* }
* @endcode
*
* @param to Storage on which data will be copied into.
* @param data Data that will be serialized and stored in @c to.
*
* See more in @ref serialization.
*/
template <class Request>
void to_bulk(Request& to, std::string_view data)
{
auto const str = std::to_string(data.size());
to += to_code(type::blob_string);
to.append(std::cbegin(str), std::cend(str));
to += separator;
to.append(std::cbegin(data), std::cend(data));
to += separator;
}
template <class Request, class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
void to_bulk(Request& to, T n)
{
auto const s = std::to_string(n);
to_bulk(to, std::string_view{s});
}
namespace detail {
auto has_response(std::string_view cmd) -> bool;
template <class T>
struct add_bulk_impl {
template <class Request>
static void add(Request& to, T const& from)
{
using namespace aedis::resp3;
to_bulk(to, from);
}
};
template <class ...Ts>
struct add_bulk_impl<std::tuple<Ts...>> {
template <class Request>
static void add(Request& to, std::tuple<Ts...> const& t)
{
auto f = [&](auto const&... vs)
{
using namespace aedis::resp3;
(to_bulk(to, vs), ...);
};
std::apply(f, t);
}
};
template <class U, class V>
struct add_bulk_impl<std::pair<U, V>> {
template <class Request>
static void add(Request& to, std::pair<U, V> const& from)
{
using namespace aedis::resp3;
to_bulk(to, from.first);
to_bulk(to, from.second);
}
};
template <class Request>
void add_header(Request& to, type t, std::size_t size)
{
auto const str = std::to_string(size);
to += to_code(t);
to.append(std::cbegin(str), std::cend(str));
to += separator;
}
template <class Request, class T>
void add_bulk(Request& to, T const& data)
{
detail::add_bulk_impl<T>::add(to, data);
}
template <class>
struct bulk_counter;
template <class>
struct bulk_counter {
static constexpr auto size = 1U;
};
template <class T, class U>
struct bulk_counter<std::pair<T, U>> {
static constexpr auto size = 2U;
};
template <class Request>
void add_blob(Request& to, std::string_view blob)
{
to.append(std::cbegin(blob), std::cend(blob));
to += separator;
}
template <class Request>
void add_separator(Request& to)
{
to += separator;
}
} // detail
/** \brief Creates Redis requests.
* \ingroup high-level-api
*
* A request is composed of one or more Redis commands and is
* referred to in the redis documentation as a pipeline, see
* https://redis.io/topics/pipelining. For example
*
* @code
* request r;
* r.push("HELLO", 3);
* r.push("FLUSHALL");
* r.push("PING");
* r.push("PING", "key");
* r.push("QUIT");
* @endcode
*
* \remarks
*
* \li Non-string types will be converted to string by using \c
* to_bulk, which must be made available over ADL.
* \li Uses a std::pmr::string for internal storage.
*/
class request {
public:
/// Request configuration options.
struct config {
/** \brief If `true`
* `aedis::connection::async_exec` will complete with error if the
* connection is lost. Affects only requests that haven't been
* sent yet.
*/
bool cancel_on_connection_lost = true;
/** \brief If `true` the request will be coalesced with other
* requests, see https://redis.io/topics/pipelining. Otherwise
* the request is sent individually.
*/
bool coalesce = true;
/** \brief If `true` the request will complete with
* aedis::error::not_connected if `async_exec` is called before
* the connection with Redis was established.
*/
bool cancel_if_not_connected = false;
/** \brief If `false` `aedis::connection::async_exec` will not
* automatically cancel this request if the connection is lost.
* Affects only requests that have been written to the socket
* but remained unresponded when `aedis::connection::async_run`
* completed.
*/
bool cancel_if_unresponded = true;
/** \brief If this request has a `HELLO` command and this flag is
* `true`, the `aedis::connection` will move it to the front of
* the queue of awaiting requests. This makes it possible to
* send `HELLO` and authenticate before other commands are sent.
*/
bool hello_with_priority = true;
};
/** \brief Constructor
*
* \param cfg Configuration options.
* \param resource Memory resource.
*/
explicit
request(config cfg = config{true, true, false, true, true},
std::pmr::memory_resource* resource = std::pmr::get_default_resource())
: cfg_{cfg}, payload_(resource) {}
//// Returns the number of commands contained in this request.
[[nodiscard]] auto size() const noexcept -> std::size_t
{ return commands_;};
[[nodiscard]] auto payload() const noexcept -> auto const&
{ return payload_;}
[[nodiscard]] auto has_hello_priority() const noexcept -> auto const&
{ return has_hello_priority_;}
/// Clears the request preserving allocated memory.
void clear()
{
payload_.clear();
commands_ = 0;
}
/// Calls std::pmr::string::reserve on the internal storage.
void reserve(std::size_t new_cap = 0)
{ payload_.reserve(new_cap); }
/// Returns a const reference to the config object.
[[nodiscard]] auto get_config() const noexcept -> auto const& {return cfg_; }
/// Returns a reference to the config object.
[[nodiscard]] auto get_config() noexcept -> auto& {return cfg_; }
/** @brief Appends a new command to the end of the request.
*
* For example
*
* \code
* request req;
* req.push("SET", "key", "some string", "EX", "2");
* \endcode
*
* will add the \c set command with value "some string" and an
* expiration of 2 seconds.
*
* \param cmd The command e.g redis or sentinel command.
* \param args Command arguments.
*/
template <class... Ts>
void push(std::string_view cmd, Ts const&... args)
{
using resp3::type;
auto constexpr pack_size = sizeof...(Ts);
detail::add_header(payload_, type::array, 1 + pack_size);
detail::add_bulk(payload_, cmd);
detail::add_bulk(payload_, std::tie(std::forward<Ts const&>(args)...));
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a key and have a
* dynamic range of arguments. For example
*
* @code
* std::map<std::string, std::string> map
* { {"key1", "value1"}
* , {"key2", "value2"}
* , {"key3", "value3"}
* };
*
* request req;
* req.push_range("HSET", "key", std::cbegin(map), std::cend(map));
* @endcode
*
* \param cmd The command e.g. Redis or Sentinel command.
* \param key The command key.
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
*/
template <class Key, class ForwardIterator>
void push_range(std::string_view cmd, Key const& key, ForwardIterator begin, ForwardIterator end,
typename std::iterator_traits<ForwardIterator>::value_type * = nullptr)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
using resp3::type;
if (begin == end)
return;
auto constexpr size = detail::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
detail::add_header(payload_, type::array, 2 + size * distance);
detail::add_bulk(payload_, cmd);
detail::add_bulk(payload_, key);
for (; begin != end; ++begin)
detail::add_bulk(payload_, *begin);
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a dynamic number
* of arguments and don't have a key. For example
*
* \code
* std::set<std::string> channels
* { "channel1" , "channel2" , "channel3" }
*
* request req;
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
* \endcode
*
* \param cmd The Redis command
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
*/
template <class ForwardIterator>
void push_range(std::string_view cmd, ForwardIterator begin, ForwardIterator end,
typename std::iterator_traits<ForwardIterator>::value_type * = nullptr)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
using resp3::type;
if (begin == end)
return;
auto constexpr size = detail::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
detail::add_header(payload_, type::array, 1 + size * distance);
detail::add_bulk(payload_, cmd);
for (; begin != end; ++begin)
detail::add_bulk(payload_, *begin);
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range (i.e. send_range2).
*
* \param cmd Redis command.
* \param key Redis key.
* \param range Range to send e.g. and \c std::map.
*/
template <class Key, class Range>
void push_range(std::string_view cmd, Key const& key, Range const& range,
decltype(std::begin(range)) * = nullptr)
{
using std::begin;
using std::end;
push_range(cmd, key, begin(range), end(range));
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range (i.e. send_range2).
*
* \param cmd Redis command.
* \param range Range to send e.g. and \c std::map.
*/
template <class Range>
void push_range(std::string_view cmd, Range const& range,
decltype(std::begin(range)) * = nullptr)
{
using std::begin;
using std::end;
push_range(cmd, begin(range), end(range));
}
private:
void check_cmd(std::string_view cmd)
{
if (!detail::has_response(cmd))
++commands_;
if (cmd == "HELLO")
has_hello_priority_ = cfg_.hello_with_priority;
}
config cfg_;
std::pmr::string payload_;
std::size_t commands_ = 0;
bool has_hello_priority_ = false;
};
} // aedis::resp3
#endif // AEDIS_RESP3_SERIALIZER_HPP

View File

@@ -1,89 +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 AEDIS_RESP3_TYPE_HPP
#define AEDIS_RESP3_TYPE_HPP
#include <ostream>
#include <vector>
#include <string>
namespace aedis::resp3 {
/** \brief RESP3 data types.
\ingroup high-level-api
The RESP3 specification can be found at https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md.
*/
enum class type
{ /// Aggregate
array,
/// Aaggregate
push,
/// Aggregate
set,
/// Aggregate
map,
/// Aggregate
attribute,
/// Simple
simple_string,
/// Simple
simple_error,
/// Simple
number,
/// Simple
doublean,
/// Simple
boolean,
/// Simple
big_number,
/// Simple
null,
/// Simple
blob_error,
/// Simple
verbatim_string,
/// Simple
blob_string,
/// Simple
streamed_string,
/// Simple
streamed_string_part,
/// Invalid
invalid
};
/** \brief Converts the data type to a string.
* \ingroup high-level-api
* \param t RESP3 type.
*/
auto to_string(type t) -> char const*;
/** \brief Writes the type to the output stream.
* \ingroup high-level-api
* \param os Output stream.
* \param t RESP3 type.
*/
auto operator<<(std::ostream& os, type t) -> std::ostream&;
/* Checks whether the data type is an aggregate.
*/
auto is_aggregate(type t) -> bool;
// For map and attribute data types this function returns 2. All
// other types have value 1.
auto element_multiplicity(type t) -> std::size_t;
// Returns the wire code of a given type.
auto to_code(type t) -> char;
// Converts a wire-format RESP3 type (char) to a resp3 type.
auto to_type(char c) -> type;
} // aedis::resp3
#endif // AEDIS_RESP3_TYPE_HPP

View File

@@ -1,64 +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 AEDIS_RESP3_WRITE_HPP
#define AEDIS_RESP3_WRITE_HPP
#include <boost/asio/write.hpp>
namespace aedis::resp3 {
/** \brief Writes a request synchronously.
* \ingroup low-level-api
*
* \param stream Stream to write the request to.
* \param req Request to write.
*/
template<
class SyncWriteStream,
class Request
>
auto write(SyncWriteStream& stream, Request const& req)
{
return boost::asio::write(stream, boost::asio::buffer(req.payload()));
}
template<
class SyncWriteStream,
class Request
>
auto write(
SyncWriteStream& stream,
Request const& req,
boost::system::error_code& ec)
{
return boost::asio::write(stream, boost::asio::buffer(req.payload()), ec);
}
/** \brief Writes a request asynchronously.
* \ingroup low-level-api
*
* \param stream Stream to write the request to.
* \param req Request to write.
* \param token Asio completion token.
*/
template<
class AsyncWriteStream,
class Request,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>
>
auto async_write(
AsyncWriteStream& stream,
Request const& req,
CompletionToken&& token =
boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>{})
{
return boost::asio::async_write(stream, boost::asio::buffer(req.payload()), token);
}
} // aedis::resp3
#endif // AEDIS_RESP3_WRITE_HPP

View File

@@ -1,160 +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 AEDIS_SSL_CONNECTION_HPP
#define AEDIS_SSL_CONNECTION_HPP
#include <aedis/detail/connection_base.hpp>
#include <boost/asio/io_context.hpp>
#include <chrono>
#include <memory>
namespace aedis::ssl {
template <class>
class basic_connection;
/** \brief A SSL connection to the Redis server.
* \ingroup high-level-api
*
* This class keeps a healthy connection to the Redis instance where
* commands can be sent at any time. For more details, please see the
* documentation of each individual function.
*
* @tparam AsyncReadWriteStream A stream that supports reading and
* writing.
*
*/
template <class AsyncReadWriteStream>
class basic_connection<boost::asio::ssl::stream<AsyncReadWriteStream>> :
private aedis::detail::connection_base<
typename boost::asio::ssl::stream<AsyncReadWriteStream>::executor_type,
basic_connection<boost::asio::ssl::stream<AsyncReadWriteStream>>> {
public:
/// Type of the next layer
using next_layer_type = boost::asio::ssl::stream<AsyncReadWriteStream>;
/// Executor type.
using executor_type = typename next_layer_type::executor_type;
/// Rebinds the socket type to another executor.
template <class Executor1>
struct rebind_executor
{
/// The socket type when rebound to the specified executor.
using other = basic_connection<boost::asio::ssl::stream<typename AsyncReadWriteStream::template rebind_executor<Executor1>::other>>;
};
using base_type = aedis::detail::connection_base<executor_type, basic_connection<boost::asio::ssl::stream<AsyncReadWriteStream>>>;
/// Constructor
explicit
basic_connection(
executor_type ex,
boost::asio::ssl::context& ctx,
std::pmr::memory_resource* resource = std::pmr::get_default_resource())
: base_type{ex, resource}
, stream_{ex, ctx}
{ }
/// Constructor
explicit
basic_connection(
boost::asio::io_context& ioc,
boost::asio::ssl::context& ctx,
std::pmr::memory_resource* resource = std::pmr::get_default_resource())
: basic_connection(ioc.get_executor(), ctx, resource)
{ }
/// Returns the associated executor.
auto get_executor() {return stream_.get_executor();}
/// Reset the underlying stream.
void reset_stream(boost::asio::ssl::context& ctx)
{
stream_ = next_layer_type{stream_.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_; }
/** @brief Establishes a connection with the Redis server asynchronously.
*
* See aedis::connection::async_run for more information.
*/
template <class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_run(CompletionToken token = CompletionToken{})
{
return base_type::async_run(std::move(token));
}
/** @brief Executes a command on the Redis server asynchronously.
*
* See aedis::connection::async_exec for more information.
*/
template <
class Adapter = aedis::detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_exec(
resp3::request const& req,
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
return base_type::async_exec(req, adapter, std::move(token));
}
/** @brief Receives server side pushes asynchronously.
*
* See aedis::connection::async_receive for detailed information.
*/
template <
class Adapter = aedis::detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_receive(
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
return base_type::async_receive(adapter, std::move(token));
}
/** @brief Cancel operations.
*
* See aedis::connection::cancel for more information.
*/
auto cancel(operation op) -> std::size_t
{ return base_type::cancel(op); }
auto& lowest_layer() noexcept { return stream_.lowest_layer(); }
private:
using this_type = basic_connection<next_layer_type>;
template <class, class> friend class aedis::detail::connection_base;
template <class, class> friend struct aedis::detail::exec_op;
template <class, class> friend struct aedis::detail::exec_read_op;
template <class> friend struct aedis::detail::run_op;
template <class> friend struct aedis::detail::writer_op;
template <class> friend struct aedis::detail::reader_op;
auto is_open() const noexcept { return stream_.next_layer().is_open(); }
void close() { stream_.next_layer().close(); }
next_layer_type stream_;
};
/** \brief A connection that uses a boost::asio::ssl::stream<boost::asio::ip::tcp::socket>.
* \ingroup high-level-api
*/
using connection = basic_connection<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>;
} // aedis::ssl
#endif // AEDIS_SSL_CONNECTION_HPP

View File

@@ -4,13 +4,16 @@
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_HPP
#define AEDIS_HPP
#ifndef BOOST_REDIS_HPP
#define BOOST_REDIS_HPP
#include <aedis/error.hpp>
#include <aedis/adapt.hpp>
#include <aedis/connection.hpp>
#include <aedis/resp3/request.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/ignore.hpp>
#include <boost/redis/logger.hpp>
/** @defgroup high-level-api Reference
*
@@ -22,4 +25,4 @@
* This page contains the documentation of the Aedis low-level API.
*/
#endif // AEDIS_HPP
#endif // BOOST_REDIS_HPP

View File

@@ -0,0 +1,80 @@
/* 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_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 <tuple>
#include <limits>
#include <string_view>
#include <variant>
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>>
*
* The types T1, T2, etc can be any STL container, any integer type
* and `std::string`.
*
* @param t Tuple containing the responses.
*/
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
*
* 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
*/
template<class T>
auto adapt2(T& t = redis::ignore) noexcept
{ return detail::result_traits<T>::adapt(t); }
} // boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_ADAPT_HPP

View File

@@ -4,15 +4,14 @@
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_ADAPTER_ADAPTERS_HPP
#define AEDIS_ADAPTER_ADAPTERS_HPP
#include <aedis/error.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/request.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/node.hpp>
#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/assert.hpp>
#include <set>
@@ -29,55 +28,63 @@
#include <string_view>
#include <charconv>
namespace aedis::adapter::detail {
// See https://stackoverflow.com/a/31658120/1077832
#include<ciso646>
#ifdef _LIBCPP_VERSION
#else
#include <cstdlib>
#endif
namespace boost::redis::adapter::detail
{
// Serialization.
template <class T>
auto from_bulk(T& i, std::string_view sv, boost::system::error_code& ec) -> typename std::enable_if<std::is_integral<T>::value, void>::type
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 = error::not_a_number;
ec = redis::error::not_a_number;
}
inline
void from_bulk(bool& t, std::string_view sv, boost::system::error_code&)
void boost_redis_from_bulk(bool& t, std::string_view sv, system::error_code&)
{
t = *sv.data() == 't';
}
inline
void from_bulk(double& d, std::string_view sv, boost::system::error_code& ec)
void boost_redis_from_bulk(double& d, std::string_view sv, 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;
#else
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), d);
if (res.ec != std::errc())
ec = error::not_a_double;
ec = redis::error::not_a_double;
#endif // _LIBCPP_VERSION
}
template <class CharT, class Traits, class Allocator>
void
from_bulk(
boost_redis_from_bulk(
std::basic_string<CharT, Traits, Allocator>& s,
std::string_view sv,
boost::system::error_code&)
system::error_code&)
{
s.append(sv.data(), sv.size());
}
//================================================
inline
void set_on_resp3_error(resp3::type t, boost::system::error_code& ec)
{
switch (t) {
case resp3::type::simple_error: ec = error::resp3_simple_error; return;
case resp3::type::blob_error: ec = error::resp3_blob_error; return;
case resp3::type::null: ec = error::resp3_null; return;
default: return;
}
}
template <class Result>
class general_aggregate {
private:
@@ -85,9 +92,17 @@ private:
public:
explicit general_aggregate(Result* c = nullptr): result_(c) {}
void operator()(resp3::node<std::string_view> const& n, boost::system::error_code&)
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code&)
{
result_->push_back({n.data_type, n.aggregate_size, n.depth, std::string{std::cbegin(n.value), std::cend(n.value)}});
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)}};
break;
default:
result_->value().push_back({nd.data_type, nd.aggregate_size, nd.depth, std::string{std::cbegin(nd.value), std::cend(nd.value)}});
}
}
};
@@ -99,13 +114,20 @@ private:
public:
explicit general_simple(Node* t = nullptr) : result_(t) {}
void operator()(resp3::node<std::string_view> const& n, boost::system::error_code& ec)
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code&)
{
result_->data_type = n.data_type;
result_->aggregate_size = n.aggregate_size;
result_->depth = n.depth;
result_->value.assign(n.value.data(), n.value.size());
set_on_resp3_error(n.data_type, ec);
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)}};
break;
default:
result_->value().data_type = nd.data_type;
result_->value().aggregate_size = nd.aggregate_size;
result_->value().depth = nd.depth;
result_->value().value.assign(nd.value.data(), nd.value.size());
}
}
};
@@ -117,19 +139,15 @@ public:
void
operator()(
Result& result,
resp3::node<std::string_view> const& n,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& n,
system::error_code& ec)
{
set_on_resp3_error(n.data_type, ec);
if (ec)
return;
if (is_aggregate(n.data_type)) {
ec = error::expects_resp3_simple_type;
ec = redis::error::expects_resp3_simple_type;
return;
}
from_bulk(result, n.value, ec);
boost_redis_from_bulk(result, n.value, ec);
}
};
@@ -145,28 +163,24 @@ public:
void
operator()(
Result& result,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (nd.data_type != resp3::type::set)
ec = error::expects_resp3_set;
ec = redis::error::expects_resp3_set;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_set;
ec = redis::error::expects_resp3_set;
return;
}
typename Result::key_type obj;
from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd.value, ec);
hint_ = result.insert(hint_, std::move(obj));
}
};
@@ -184,33 +198,29 @@ public:
void
operator()(
Result& result,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (element_multiplicity(nd.data_type) != 2)
ec = error::expects_resp3_map;
ec = redis::error::expects_resp3_map;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_map;
ec = redis::error::expects_resp3_map;
return;
}
if (on_key_) {
typename Result::key_type obj;
from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd.value, ec);
current_ = result.insert(current_, {std::move(obj), {}});
} else {
typename Result::mapped_type obj;
from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd.value, ec);
current_->second = std::move(obj);
}
@@ -226,19 +236,15 @@ public:
void
operator()(
Result& result,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
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({});
from_bulk(result.back(), nd.value, ec);
boost_redis_from_bulk(result.back(), nd.value, ec);
}
}
};
@@ -254,31 +260,27 @@ public:
void
operator()(
Result& result,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (i_ != -1) {
ec = error::nested_aggregate_not_supported;
ec = redis::error::nested_aggregate_not_supported;
return;
}
if (result.size() != nd.aggregate_size * element_multiplicity(nd.data_type)) {
ec = error::incompatible_size;
ec = redis::error::incompatible_size;
return;
}
} else {
if (i_ == -1) {
ec = error::expects_resp3_aggregate;
ec = redis::error::expects_resp3_aggregate;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
from_bulk(result.at(i_), nd.value, ec);
boost_redis_from_bulk(result.at(i_), nd.value, ec);
}
++i_;
@@ -293,22 +295,18 @@ struct list_impl {
void
operator()(
Result& result,
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (!is_aggregate(nd.data_type)) {
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_aggregate;
ec = redis::error::expects_resp3_aggregate;
return;
}
result.push_back({});
from_bulk(result.back(), nd.value, ec);
boost_redis_from_bulk(result.back(), nd.value, ec);
}
}
};
@@ -356,52 +354,106 @@ struct impl_map<std::deque<T, Allocator>> { using type = list_impl<std::deque<T,
//---------------------------------------------------
template <class>
class wrapper;
template <class Result>
class wrapper {
class wrapper<result<Result>> {
public:
using response_type = result<Result>;
private:
Result* result_;
response_type* result_;
typename impl_map<Result>::type impl_;
public:
explicit wrapper(Result* t = nullptr) : result_(t)
{ impl_.on_value_available(*result_); }
void
operator()(
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
bool set_if_resp3_error(resp3::basic_node<std::string_view> const& nd) noexcept
{
BOOST_ASSERT(result_);
impl_(*result_, nd, ec);
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)}};
return true;
default:
return false;
}
}
};
template <class T>
class wrapper<std::optional<T>> {
private:
std::optional<T>* result_;
typename impl_map<T>::type impl_{};
public:
explicit wrapper(std::optional<T>* o = nullptr) : result_(o) {}
explicit wrapper(response_type* t = nullptr) : result_(t)
{
if (result_) {
result_->value() = Result{};
impl_.on_value_available(result_->value());
}
}
void
operator()(
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
if (nd.data_type == resp3::type::null)
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
if (result_->has_error())
return;
if (!result_->has_value()) {
*result_ = T{};
impl_.on_value_available(result_->value());
}
if (set_if_resp3_error(nd))
return;
BOOST_ASSERT(result_);
impl_(result_->value(), nd, ec);
}
};
} // aedis::adapter:.detail
template <class T>
class wrapper<result<std::optional<T>>> {
public:
using response_type = result<std::optional<T>>;
#endif // AEDIS_ADAPTER_ADAPTERS_HPP
private:
response_type* result_;
typename impl_map<T>::type impl_{};
bool set_if_resp3_error(resp3::basic_node<std::string_view> 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)}};
return true;
default:
return false;
}
}
public:
explicit wrapper(response_type* o = nullptr) : result_(o) {}
void
operator()(
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
if (result_->has_error())
return;
if (set_if_resp3_error(nd))
return;
if (nd.data_type == resp3::type::null)
return;
if (!result_->value().has_value()) {
result_->value() = T{};
impl_.on_value_available(result_->value().value());
}
impl_(result_->value().value(), nd, ec);
}
};
} // boost::redis::adapter::detail
#endif // BOOST_REDIS_ADAPTER_ADAPTERS_HPP

View File

@@ -0,0 +1,164 @@
/* 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_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
#define BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_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 <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);}
};
template <class Response>
class static_adapter {
private:
static constexpr auto size = std::tuple_size<Response>::value;
using adapter_tuple = mp11::mp_transform<adapter_t, Response>;
using variant_type = mp11::mp_rename<adapter_tuple, std::variant>;
using adapters_array_type = std::array<variant_type, size>;
adapters_array_type adapters_;
public:
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)
{
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));
}
};
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)
{
adapter_(nd, ec);
}
};
template <class>
struct response_traits;
template <>
struct response_traits<ignore_t> {
using response_type = ignore_t;
using adapter_type = detail::ignore_adapter;
static auto adapt(response_type&) noexcept
{ return detail::ignore_adapter{}; }
};
template <>
struct response_traits<result<ignore_t>> {
using response_type = result<ignore_t>;
using adapter_type = detail::ignore_adapter;
static auto adapt(response_type&) noexcept
{ return detail::ignore_adapter{}; }
};
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>;
static auto adapt(response_type& v) noexcept
{ return adapter_type{v}; }
};
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}; }
};
template <class Adapter>
class wrapper {
public:
explicit wrapper(Adapter adapter) : adapter_{adapter} {}
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

View File

@@ -4,14 +4,15 @@
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_ADAPTER_RESPONSE_TRAITS_HPP
#define AEDIS_ADAPTER_RESPONSE_TRAITS_HPP
#include <aedis/error.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/read.hpp>
#include <aedis/adapter/detail/adapters.hpp>
#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/mp11.hpp>
#include <vector>
@@ -19,56 +20,54 @@
#include <string_view>
#include <variant>
namespace aedis::adapter::detail {
using ignore = std::decay_t<decltype(std::ignore)>;
namespace boost::redis::adapter::detail
{
/* Traits class for response objects.
*
* Provides traits for all supported response types i.e. all STL
* containers and C++ buil-in types.
*/
template <class ResponseType>
struct response_traits {
using adapter_type = adapter::detail::wrapper<typename std::decay<ResponseType>::type>;
static auto adapt(ResponseType& r) noexcept { return adapter_type{&r}; }
template <class Result>
struct result_traits {
using adapter_type = adapter::detail::wrapper<typename std::decay<Result>::type>;
static auto adapt(Result& r) noexcept { return adapter_type{&r}; }
};
template <>
struct response_traits<ignore> {
using response_type = ignore;
using adapter_type = resp3::detail::ignore_response;
struct result_traits<result<ignore_t>> {
using response_type = result<ignore_t>;
using adapter_type = ignore;
static auto adapt(response_type) noexcept { return adapter_type{}; }
};
template <>
struct result_traits<ignore_t> {
using response_type = ignore_t;
using adapter_type = ignore;
static auto adapt(response_type) noexcept { return adapter_type{}; }
};
template <class T>
struct response_traits<resp3::node<T>> {
using response_type = resp3::node<T>;
struct result_traits<result<resp3::basic_node<T>>> {
using response_type = result<resp3::basic_node<T>>;
using adapter_type = adapter::detail::general_simple<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class String, class Allocator>
struct response_traits<std::vector<resp3::node<String>, Allocator>> {
using response_type = std::vector<resp3::node<String>, Allocator>;
struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<void> {
using response_type = void;
using adapter_type = resp3::detail::ignore_response;
static auto adapt() noexcept { return adapter_type{}; }
};
template <class T>
using adapter_t = typename response_traits<std::decay_t<T>>::adapter_type;
using adapter_t = typename result_traits<std::decay_t<T>>::adapter_type;
// Duplicated here to avoid circular include dependency.
template<class T>
auto internal_adapt(T& t) noexcept
{ return response_traits<std::decay_t<T>>::adapt(t); }
{ return result_traits<std::decay_t<T>>::adapt(t); }
template <std::size_t N>
struct assigner {
@@ -90,12 +89,15 @@ struct assigner<0> {
};
template <class Tuple>
class static_aggregate_adapter {
class static_aggregate_adapter;
template <class Tuple>
class static_aggregate_adapter<result<Tuple>> {
private:
using adapters_array_type =
std::array<
boost::mp11::mp_rename<
boost::mp11::mp_transform<
mp11::mp_rename<
mp11::mp_transform<
adapter_t, Tuple>,
std::variant>,
std::tuple_size<Tuple>::value>;
@@ -103,14 +105,18 @@ private:
std::size_t i_ = 0;
std::size_t aggregate_size_ = 0;
adapters_array_type adapters_;
result<Tuple>* res_ = nullptr;
public:
explicit static_aggregate_adapter(Tuple* r = nullptr)
explicit static_aggregate_adapter(result<Tuple>* r = nullptr)
{
detail::assigner<std::tuple_size<Tuple>::value - 1>::assign(adapters_, *r);
if (r) {
res_ = r;
detail::assigner<std::tuple_size<Tuple>::value - 1>::assign(adapters_, r->value());
}
}
void count(resp3::node<std::string_view> const& nd)
void count(resp3::basic_node<std::string_view> const& nd)
{
if (nd.depth == 1) {
if (is_aggregate(nd.data_type))
@@ -125,17 +131,14 @@ public:
++i_;
}
void
operator()(
resp3::node<std::string_view> const& nd,
boost::system::error_code& ec)
void operator()(resp3::basic_node<std::string_view> const& nd, 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 (real_aggr_size != std::tuple_size<Tuple>::value)
ec = error::incompatible_size;
ec = redis::error::incompatible_size;
return;
}
@@ -146,13 +149,13 @@ public:
};
template <class... Ts>
struct response_traits<std::tuple<Ts...>>
struct result_traits<result<std::tuple<Ts...>>>
{
using response_type = 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}; }
};
} // aedis::adapter::detail
} // boost::redis::adapter::detail
#endif // AEDIS_ADAPTER_RESPONSE_TRAITS_HPP
#endif // BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP

View File

@@ -0,0 +1,37 @@
/* 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_ADAPTER_IGNORE_HPP
#define BOOST_REDIS_ADAPTER_IGNORE_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/error.hpp>
#include <boost/system/error_code.hpp>
#include <string>
namespace boost::redis::adapter
{
/** @brief An adapter that ignores responses
* @ingroup high-level-api
*
* RESP3 errors won't be ignored.
*/
struct ignore {
void operator()(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:;
}
}
};
} // boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_IGNORE_HPP

View File

@@ -0,0 +1,81 @@
/* 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_ADAPTER_RESULT_HPP
#define BOOST_REDIS_ADAPTER_RESULT_HPP
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/error.hpp>
#include <boost/system/result.hpp>
#include <string>
namespace boost::redis::adapter
{
/** @brief Stores any resp3 error
* @ingroup high-level-api
*/
struct error {
/// RESP3 error data type.
resp3::type data_type = resp3::type::invalid;
/// Diagnostic error message sent by Redis.
std::string diagnostic;
};
/** @brief Compares two error objects for equality
* @relates error
*
* @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.data_type == b.data_type && a.diagnostic == b.diagnostic;
}
/** @brief Compares two error objects for difference
* @relates error
*
* @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);
}
/** @brief Stores response to individual Redis commands
* @ingroup high-level-api
*/
template <class Value>
using result = system::result<Value, error>;
BOOST_NORETURN inline void
throw_exception_from_error(error const & e, boost::source_location const &)
{
system::error_code ec;
switch (e.data_type) {
case resp3::type::simple_error:
ec = redis::error::resp3_simple_error;
break;
case resp3::type::blob_error:
ec = redis::error::resp3_blob_error;
break;
case resp3::type::null:
ec = redis::error::resp3_null;
break;
default:
BOOST_ASSERT_MSG(false, "Unexpected data type.");
}
throw system::system_error(ec, e.diagnostic);
}
} // boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP

View File

@@ -0,0 +1,85 @@
/* 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_CONFIG_HPP
#define BOOST_REDIS_CONFIG_HPP
#include <string>
#include <chrono>
#include <optional>
namespace boost::redis
{
/** @brief Address of a Redis server
* @ingroup high-level-api
*/
struct address {
/// Redis host.
std::string host = "127.0.0.1";
/// Redis port.
std::string port = "6379";
};
/** @brief Configure parameters used by the connection classes
* @ingroup high-level-api
*/
struct config {
/// Uses SSL instead of a plain connection.
bool use_ssl = false;
/// Address 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.
*/
std::string username;
/** @brief Password passed to the
* [HELLO](https://redis.io/commands/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.
std::string clientname = "Boost.Redis";
/// Database that will be passed to the [SELECT](https://redis.io/commands/hello/) command.
std::optional<int> database_index = 0;
/// Message used by the health-checker in `boost::redis::connection::async_run`.
std::string health_check_id = "Boost.Redis";
/// Logger prefix, see `boost::redis::logger`.
std::string log_prefix = "(Boost.Redis) ";
/// Time the resolve operation is allowed to last.
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
/// Time the connect operation is allowed to last.
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
/// Time the SSL handshake operation is allowed to last.
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
/** Health checks interval.
*
* 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.
*/
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
};
} // boost::redis
#endif // BOOST_REDIS_CONFIG_HPP

View File

@@ -0,0 +1,388 @@
/* 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_HPP
#define BOOST_REDIS_CONNECTION_HPP
#include <boost/redis/detail/connection_base.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/config.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/any_completion_handler.hpp>
#include <chrono>
#include <memory>
#include <limits>
namespace boost::redis {
namespace detail
{
template <class Connection, class Logger>
struct reconnection_op {
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
{
BOOST_ASIO_CORO_YIELD
conn_->impl_.async_run(conn_->cfg_, logger_, std::move(self));
conn_->cancel(operation::receive);
logger_.on_connection_lost(ec);
if (!conn_->will_reconnect() || is_cancelled(self)) {
conn_->cancel(operation::reconnection);
self.complete(!!ec ? ec : asio::error::operation_aborted);
return;
}
conn_->timer_.expires_after(conn_->cfg_.reconnect_wait_interval);
BOOST_ASIO_CORO_YIELD
conn_->timer_.async_wait(std::move(self));
BOOST_REDIS_CHECK_OP0(;)
if (!conn_->will_reconnect()) {
self.complete(asio::error::operation_aborted);
return;
}
conn_->reset_stream();
}
}
};
} // detail
/** @brief A SSL connection to the Redis server.
* @ingroup high-level-api
*
* This class keeps a healthy connection to the Redis instance where
* commands can be sent at any time. For more details, please see the
* documentation of each individual function.
*
* @tparam Socket The socket type e.g. asio::ip::tcp::socket.
*
*/
template <class Executor>
class basic_connection {
public:
/// Executor type.
using executor_type = Executor;
/// Returns the underlying executor.
executor_type get_executor() noexcept
{ return impl_.get_executor(); }
/// Rebinds the socket type to another executor.
template <class Executor1>
struct rebind_executor
{
/// The connection type when rebound to the specified executor.
using other = basic_connection<Executor1>;
};
/// Contructs from an executor.
explicit
basic_connection(
executor_type ex,
asio::ssl::context::method method = asio::ssl::context::tls_client,
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)())
: impl_{ex, method, max_read_size}
, timer_{ex}
{ }
/// Contructs from a context.
explicit
basic_connection(
asio::io_context& ioc,
asio::ssl::context::method method = asio::ssl::context::tls_client,
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)())
: basic_connection(ioc.get_executor(), method, max_read_size)
{ }
/** @brief Starts underlying connection operations.
*
* This member function provides the following functionality
*
* 1. Resolve the address passed on `boost::redis::config::addr`.
* 2. Connect to one of the results obtained in the resolve operation.
* 3. Send a [HELLO](https://redis.io/commands/hello/) command where each of its parameters are read from `cfg`.
* 4. Start a health-check operation where ping commands are sent
* at intervals specified in
* `boost::redis::config::health_check_interval`. The message passed to
* `PING` will be `boost::redis::config::health_check_id`. Passing a
* timeout with value zero will disable health-checks. If the Redis
* server does not respond to a health-check within two times the value
* specified here, it will be considered unresponsive and the connection
* will be closed and a new connection will be stablished.
* 5. Starts read and write operations with the Redis
* server. More specifically it will trigger the write of all
* requests i.e. calls to `async_exec` that happened prior to this
* call.
*
* When a connection is lost for any reason, a new one is
* stablished automatically. To disable reconnection call
* `boost::redis::connection::cancel(operation::reconnection)`.
*
* @param cfg Configuration paramters.
* @param l Logger object. The interface expected is specified in the class `boost::redis::logger`.
* @param token Completion token.
*
* The completion token must have the following signature
*
* @code
* void f(system::error_code);
* @endcode
*
* For example on how to call this function refer to
* cpp20_intro.cpp or any other example.
*/
template <
class Logger = logger,
class CompletionToken = asio::default_completion_token_t<executor_type>>
auto
async_run(
config const& cfg = {},
Logger l = Logger{},
CompletionToken token = CompletionToken{})
{
using this_type = basic_connection<executor_type>;
cfg_ = cfg;
l.set_prefix(cfg_.log_prefix);
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(detail::reconnection_op<this_type, Logger>{this, l}, token, timer_);
}
/** @brief Receives server side pushes asynchronously.
*
* When pushes arrive and there is no `async_receive` operation in
* progress, pushed data, requests, and responses will be paused
* until `async_receive` is called again. Apps will usually want
* to call `async_receive` in a loop.
*
* To cancel an ongoing receive operation apps should call
* `connection::cancel(operation::receive)`.
*
* @param response Response object.
* @param token Completion token.
*
* For an example see cpp20_subscriber.cpp. The completion token must
* have the following signature
*
* @code
* void f(system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the push received in
* bytes.
*/
template <
class Response = ignore_t,
class CompletionToken = asio::default_completion_token_t<executor_type>
>
auto
async_receive(
Response& response,
CompletionToken token = CompletionToken{})
{
return impl_.async_receive(response, token);
}
/** @brief Executes commands on the Redis server asynchronously.
*
* This function sends a request to the Redis server and waits for
* the responses to each individual command in the request. If the
* request contains only commands that don't expect a response,
* the completion occurs after it has been written to the
* underlying stream. Multiple concurrent calls to this function
* will be automatically queued by the implementation.
*
* @param req Request.
* @param resp Response.
* @param token Completion token.
*
* For an example see cpp20_echo_server.cpp. The completion token must
* have the following signature
*
* @code
* void f(system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the response received
* in bytes.
*/
template <
class Response = ignore_t,
class CompletionToken = asio::default_completion_token_t<executor_type>
>
auto
async_exec(
request const& req,
Response& resp = ignore,
CompletionToken&& token = CompletionToken{})
{
return impl_.async_exec(req, resp, std::forward<CompletionToken>(token));
}
/** @brief Cancel operations.
*
* @li `operation::exec`: Cancels operations started with
* `async_exec`. Affects only requests that haven't been written
* yet.
* @li operation::run: Cancels the `async_run` operation.
* @li operation::receive: Cancels any ongoing calls to `async_receive`.
* @li operation::all: Cancels all operations listed above.
*
* @param op: The operation to be cancelled.
* @returns The number of operations that have been canceled.
*/
void cancel(operation op = operation::all)
{
switch (op) {
case operation::reconnection:
case operation::all:
cfg_.reconnect_wait_interval = std::chrono::seconds::zero();
timer_.cancel();
break;
default: /* ignore */;
}
impl_.cancel(op);
}
/// Returns true if the connection was canceled.
bool will_reconnect() const noexcept
{ return cfg_.reconnect_wait_interval != std::chrono::seconds::zero();}
/// Returns the ssl context.
auto const& get_ssl_context() const noexcept
{ return impl_.get_ssl_context();}
/// Returns the ssl context.
auto& get_ssl_context() noexcept
{ return impl_.get_ssl_context();}
/// Resets the underlying stream.
void reset_stream()
{ impl_.reset_stream(); }
/// Returns a reference to the next layer.
auto& next_layer() noexcept
{ return impl_.next_layer(); }
/// Returns a const reference to the next layer.
auto const& next_layer() const noexcept
{ return impl_.next_layer(); }
private:
using timer_type =
asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
template <class, class> friend struct detail::reconnection_op;
config cfg_;
detail::connection_base<executor_type> impl_;
timer_type timer_;
};
/** \brief A basic_connection that type erases the executor.
* \ingroup high-level-api
*
* This connection type uses the asio::any_io_executor and
* asio::any_completion_token to reduce compilation times.
*
* For documentaiton of each member function see
* `boost::redis::basic_connection`.
*/
class connection {
public:
/// Executor type.
using executor_type = asio::any_io_executor;
/// Contructs from an executor.
explicit
connection(
executor_type ex,
asio::ssl::context::method method = asio::ssl::context::tls_client,
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)());
/// Contructs from a context.
explicit
connection(
asio::io_context& ioc,
asio::ssl::context::method method = asio::ssl::context::tls_client,
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)());
/// Returns the underlying executor.
executor_type get_executor() noexcept
{ return impl_.get_executor(); }
/// Calls `boost::redis::basic_connection::async_run`.
template <class CompletionToken>
auto async_run(config const& cfg, logger l, CompletionToken token)
{
return asio::async_initiate<
CompletionToken, void(boost::system::error_code)>(
[](auto handler, connection* self, config const* cfg, logger l)
{
self->async_run_impl(*cfg, l, std::move(handler));
}, token, this, &cfg, l);
}
/// Calls `boost::redis::basic_connection::async_receive`.
template <class Response, class CompletionToken>
auto async_receive(Response& response, CompletionToken token)
{
return impl_.async_receive(response, std::move(token));
}
/// Calls `boost::redis::basic_connection::async_exec`.
template <class Response, class CompletionToken>
auto async_exec(request const& req, Response& resp, CompletionToken token)
{
return impl_.async_exec(req, resp, std::move(token));
}
/// Calls `boost::redis::basic_connection::cancel`.
void cancel(operation op = operation::all);
/// Calls `boost::redis::basic_connection::will_reconnect`.
bool will_reconnect() const noexcept
{ return impl_.will_reconnect();}
/// Calls `boost::redis::basic_connection::next_layer`.
auto& next_layer() noexcept
{ return impl_.next_layer(); }
/// Calls `boost::redis::basic_connection::next_layer`.
auto const& next_layer() const noexcept
{ return impl_.next_layer(); }
/// Calls `boost::redis::basic_connection::reset_stream`.
void reset_stream()
{ impl_.reset_stream();}
private:
void
async_run_impl(
config const& cfg,
logger l,
asio::any_completion_handler<void(boost::system::error_code)> token);
basic_connection<executor_type> impl_;
};
} // boost::redis
#endif // BOOST_REDIS_CONNECTION_HPP

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,224 @@
/* 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_HEALTH_CHECKER_HPP
#define BOOST_REDIS_HEALTH_CHECKER_HPP
// Has to included before promise.hpp to build on msvc.
#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 <chrono>
namespace boost::redis::detail {
template <class HealthChecker, class Connection>
class ping_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
{
if (checker_->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();)
// 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(;)
}
}
};
template <class HealthChecker, class Connection>
class check_timeout_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
{
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 (checker_->resp_.has_error()) {
self.complete({});
return;
}
if (checker_->resp_.value().empty()) {
checker_->ping_timer_.cancel();
conn_->cancel(operation::run);
checker_->checker_has_exited_ = true;
self.complete(error::pong_timeout);
return;
}
if (checker_->resp_.has_value()) {
checker_->resp_.value().clear();
}
}
}
};
template <class 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>;
public:
health_checker(Executor ex)
: ping_timer_{ex}
, wait_timer_{ex}
{
req_.push("PING", "Boost.Redis");
}
void set_config(config const& cfg)
{
req_.clear();
req_.push("PING", cfg.health_check_id);
ping_interval_ = cfg.health_check_interval;
}
template <
class Connection,
class CompletionToken = asio::default_completion_token_t<Executor>
>
auto async_check_health(Connection& conn, CompletionToken token = CompletionToken{})
{
checker_has_exited_ = false;
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(check_health_op<health_checker, Connection>{this, &conn}, token, conn);
}
std::size_t cancel(operation op)
{
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_);
}
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_);
}
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_;
redis::request req_;
redis::generic_response resp_;
std::chrono::steady_clock::duration ping_interval_ = std::chrono::seconds{5};
bool checker_has_exited_ = false;
};
} // boost::redis::detail
#endif // BOOST_REDIS_HEALTH_CHECKER_HPP

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
/* 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_WRITE_HPP
#define BOOST_REDIS_WRITE_HPP
#include <boost/asio/write.hpp>
#include <boost/redis/request.hpp>
namespace boost::redis::detail {
/** \brief Writes a request synchronously.
* \ingroup low-level-api
*
* \param stream Stream to write the request to.
* \param req Request to write.
*/
template<class SyncWriteStream>
auto write(SyncWriteStream& stream, request const& req)
{
return asio::write(stream, asio::buffer(req.payload()));
}
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
*
* \param stream Stream to write the request to.
* \param req Request to write.
* \param token Asio completion token.
*/
template<
class AsyncWriteStream,
class CompletionToken = asio::default_completion_token_t<typename AsyncWriteStream::executor_type>
>
auto async_write(
AsyncWriteStream& stream,
request const& req,
CompletionToken&& token =
asio::default_completion_token_t<typename AsyncWriteStream::executor_type>{})
{
return asio::async_write(stream, asio::buffer(req.payload()), token);
}
} // boost::redis::detail
#endif // BOOST_REDIS_WRITE_HPP

View File

@@ -4,12 +4,12 @@
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_ERROR_HPP
#define AEDIS_ERROR_HPP
#ifndef BOOST_REDIS_ERROR_HPP
#define BOOST_REDIS_ERROR_HPP
#include <boost/system/error_code.hpp>
namespace aedis {
namespace boost::redis {
/** \brief Generic errors.
* \ingroup high-level-api
@@ -63,6 +63,18 @@ enum class error
/// There is no stablished connection.
not_connected,
/// Resolve timeout
resolve_timeout,
/// Connect timeout
connect_timeout,
/// Connect timeout
pong_timeout,
/// SSL handshake timeout
ssl_handshake_timeout,
};
/** \internal
@@ -70,15 +82,15 @@ enum class error
* \param e Error code.
* \ingroup any
*/
auto make_error_code(error e) -> boost::system::error_code;
auto make_error_code(error e) -> system::error_code;
} // aedis
} // boost::redis
namespace std {
template<>
struct is_error_code_enum<::aedis::error> : std::true_type {};
struct is_error_code_enum<::boost::redis::error> : std::true_type {};
} // std
#endif // AEDIS_ERROR_HPP
#endif // BOOST_REDIS_ERROR_HPP

View File

@@ -0,0 +1,49 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_IGNORE_HPP
#define BOOST_REDIS_IGNORE_HPP
#include <boost/system/result.hpp>
#include <tuple>
#include <type_traits>
namespace boost::redis
{
/** @brief Type used to ignore responses.
* @ingroup high-level-api
*
* For example
*
* @code
* response<ignore_t, std::string, ignore_t> resp;
* @endcode
*
* 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
*
* @code
* conn->async_exec(req, ignore, ...);
* @endcode
*
* RESP3 errors won't be ignore but will cause `async_exec` to
* complete with an error.
*/
extern ignore_t ignore;
} // boost::redis
#endif // BOOST_REDIS_IGNORE_HPP

View File

@@ -0,0 +1,39 @@
/* 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>
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}
{ }
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(
config const& cfg,
logger l,
asio::any_completion_handler<void(boost::system::error_code)> token)
{
impl_.async_run(cfg, l, std::move(token));
}
void connection::cancel(operation op)
{
impl_.cancel(op);
}
} // namespace boost::redis

View File

@@ -4,26 +4,26 @@
* accompanying file LICENSE.txt)
*/
#include <aedis/error.hpp>
#include <boost/redis/error.hpp>
#include <boost/assert.hpp>
namespace aedis {
namespace boost::redis {
namespace detail {
struct error_category_impl : boost::system::error_category {
struct error_category_impl : system::error_category {
virtual ~error_category_impl() = default;
auto name() const noexcept -> char const* override
{
return "aedis";
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.";
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.";
@@ -38,12 +38,15 @@ struct error_category_impl : boost::system::error_category {
case error::not_a_double: return "Not a double.";
case error::resp3_null: return "Got RESP3 null.";
case error::not_connected: return "Not connected.";
default: BOOST_ASSERT(false); return "Aedis error.";
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.";
}
}
};
auto category() -> boost::system::error_category const&
auto category() -> system::error_category const&
{
static error_category_impl instance;
return instance;
@@ -51,9 +54,9 @@ auto category() -> boost::system::error_category const&
} // detail
auto make_error_code(error e) -> boost::system::error_code
auto make_error_code(error e) -> system::error_code
{
return boost::system::error_code{static_cast<int>(e), detail::category()};
return system::error_code{static_cast<int>(e), detail::category()};
}
} // aedis
} // boost::redis::detail

View File

@@ -4,7 +4,9 @@
* accompanying file LICENSE.txt)
*/
#include <aedis/impl/error.ipp>
#include <aedis/resp3/impl/request.ipp>
#include <aedis/resp3/impl/type.ipp>
#include <aedis/resp3/detail/impl/parser.ipp>
#include <boost/redis/ignore.hpp>
namespace boost::redis
{
ignore_t ignore;
}

View File

@@ -0,0 +1,128 @@
/* 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/logger.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
#include <iterator>
namespace boost::redis
{
void logger::write_prefix()
{
if (!std::empty(prefix_))
std::clog << prefix_;
}
void logger::on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res)
{
if (level_ < level::info)
return;
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

View File

@@ -4,11 +4,11 @@
* accompanying file LICENSE.txt)
*/
#include <aedis/resp3/request.hpp>
#include <boost/redis/request.hpp>
#include <string_view>
namespace aedis::resp3::detail {
namespace boost::redis::detail {
auto has_response(std::string_view cmd) -> bool
{
@@ -18,4 +18,4 @@ auto has_response(std::string_view cmd) -> bool
return false;
}
} // aedis::resp3::detail
} // boost:redis::detail

View File

@@ -0,0 +1,127 @@
/* 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_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;}
namespace boost::redis {
/** @brief Logger class
* @ingroup high-level-api
*
* The class can be passed to the connection objects to log to `std::clog`
*/
class logger {
public:
/** @brief Syslog-like log levels
* @ingroup high-level-api
*/
enum class level
{ /// Emergency
emerg,
/// Alert
alert,
/// Critical
crit,
/// Error
err,
/// Warning
warning,
/// Notice
notice,
/// Info
info,
/// Debug
debug
};
/** @brief Constructor
* @ingroup high-level-api
*
* @param l Log level.
*/
logger(level l = level::info)
: level_{l}
{}
/** @brief Called when the resolve operation completes.
* @ingroup high-level-api
*
* @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 ec Error returned by the connect operation.
* @param ep Endpoint to which the connection connected.
*/
void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep);
/** @brief Called when the ssl handshake operation completes.
* @ingroup high-level-api
*
* @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
*
* @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 ec Error code returned by the write operation.
* @param payload The payload written to the socket.
*/
void on_write(system::error_code const& ec, std::string const& payload);
/** @brief Called when the `HELLO` request completes.
* @ingroup high-level-api
*
* @param ec Error code returned by the async_exec operation.
* @param resp Response sent by the Redis server.
*/
void on_hello(system::error_code const& ec, generic_response const& resp);
/** @brief Sets a prefix to every log message
* @ingroup high-level-api
*
* @param prefix The prefix.
*/
void set_prefix(std::string_view prefix)
{
prefix_ = prefix;
}
private:
void write_prefix();
level level_;
std::string_view prefix_;
};
} // boost::redis
#endif // BOOST_REDIS_LOGGER_HPP

View File

@@ -0,0 +1,41 @@
/* 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_OPERATION_HPP
#define BOOST_REDIS_OPERATION_HPP
namespace boost::redis {
/** @brief Connection operations that can be cancelled.
* @ingroup high-level-api
*
* The operations listed below can be passed to the
* `boost::redis::connection::cancel` member function.
*/
enum class operation {
/// Resolve operation.
resolve,
/// Connect operation.
connect,
/// SSL handshake operation.
ssl_handshake,
/// Refers to `connection::async_exec` operations.
exec,
/// Refers to `connection::async_run` operations.
run,
/// Refers to `connection::async_receive` operations.
receive,
/// Cancels reconnection.
reconnection,
/// Health check operation.
health_check,
/// Refers to all operations.
all,
};
} // boost::redis
#endif // BOOST_REDIS_OPERATION_HPP

View File

@@ -0,0 +1,321 @@
/* 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_REQUEST_HPP
#define BOOST_REDIS_REQUEST_HPP
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <string>
#include <tuple>
#include <algorithm>
// NOTE: For some commands like hset it would be a good idea to assert
// the value type is a pair.
namespace boost::redis {
namespace detail{
auto has_response(std::string_view cmd) -> bool;
}
/** \brief Creates Redis requests.
* \ingroup high-level-api
*
* A request is composed of one or more Redis commands and is
* referred to in the redis documentation as a pipeline, see
* https://redis.io/topics/pipelining. For example
*
* @code
* request r;
* r.push("HELLO", 3);
* r.push("FLUSHALL");
* r.push("PING");
* r.push("PING", "key");
* r.push("QUIT");
* @endcode
*
* \remarks
*
* Uses a std::string for internal storage.
*/
class request {
public:
/// Request configuration options.
struct config {
/** \brief If `true`
* `boost::redis::connection::async_exec` will complete with error if the
* connection is lost. Affects only requests that haven't been
* sent yet.
*/
bool cancel_on_connection_lost = true;
/** \brief If `true` the request will complete with
* boost::redis::error::not_connected if `async_exec` is called before
* the connection with Redis was established.
*/
bool cancel_if_not_connected = false;
/** \brief If `false` `boost::redis::connection::async_exec` will not
* automatically cancel this request if the connection is lost.
* Affects only requests that have been written to the socket
* but remained unresponded when `boost::redis::connection::async_run`
* completed.
*/
bool cancel_if_unresponded = true;
/** \brief If this request has a `HELLO` command and this flag is
* `true`, the `boost::redis::connection` will move it to the front of
* the queue of awaiting requests. This makes it possible to
* send `HELLO` and authenticate before other commands are sent.
*/
bool hello_with_priority = true;
};
/** \brief Constructor
*
* \param cfg Configuration options.
*/
explicit
request(config cfg = config{true, false, true, true})
: cfg_{cfg} {}
//// Returns the number of commands contained in this request.
[[nodiscard]] auto size() const noexcept -> std::size_t
{ return commands_;};
[[nodiscard]] auto payload() const noexcept -> std::string_view
{ return payload_;}
[[nodiscard]] auto has_hello_priority() const noexcept -> auto const&
{ return has_hello_priority_;}
/// Clears the request preserving allocated memory.
void clear()
{
payload_.clear();
commands_ = 0;
has_hello_priority_ = false;
}
/// Calls std::string::reserve on the internal storage.
void reserve(std::size_t new_cap = 0)
{ payload_.reserve(new_cap); }
/// Returns a const reference to the config object.
[[nodiscard]] auto get_config() const noexcept -> auto const& {return cfg_; }
/// Returns a reference to the config object.
[[nodiscard]] auto get_config() noexcept -> auto& {return cfg_; }
/** @brief Appends a new command to the end of the request.
*
* For example
*
* \code
* request req;
* req.push("SET", "key", "some string", "EX", "2");
* \endcode
*
* will add the `set` command with value "some string" and an
* expiration of 2 seconds.
*
* \param cmd The command e.g redis or sentinel command.
* \param args Command arguments.
* \tparam Ts Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument. This function must be made available over ADL and must have the following signature
*
* @code
* void boost_redis_to_bulk(std::string& to, T const& t);
* {
* boost::redis::resp3::boost_redis_to_bulk(to, serialize(t));
* }
* @endcode
*
* See cpp20_serialization.cpp
*/
template <class... Ts>
void push(std::string_view cmd, Ts const&... args)
{
auto constexpr pack_size = sizeof...(Ts);
resp3::add_header(payload_, resp3::type::array, 1 + pack_size);
resp3::add_bulk(payload_, cmd);
resp3::add_bulk(payload_, std::tie(std::forward<Ts const&>(args)...));
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a key and have a
* dynamic range of arguments. For example
*
* @code
* std::map<std::string, std::string> map
* { {"key1", "value1"}
* , {"key2", "value2"}
* , {"key3", "value3"}
* };
*
* request req;
* req.push_range("HSET", "key", std::cbegin(map), std::cend(map));
* @endcode
*
* \param cmd The command e.g. Redis or Sentinel command.
* \param key The command key.
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
* \tparam Ts Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument. This function must be made available over ADL and must have the following signature
*
* @code
* void boost_redis_to_bulk(std::string& to, T const& t);
* {
* boost::redis::resp3::boost_redis_to_bulk(to, serialize(t));
* }
* @endcode
*
* See cpp20_serialization.cpp
*/
template <class ForwardIterator>
void
push_range(
std::string_view const& cmd,
std::string_view const& key,
ForwardIterator begin,
ForwardIterator end,
typename std::iterator_traits<ForwardIterator>::value_type * = nullptr)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
if (begin == end)
return;
auto constexpr size = resp3::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
resp3::add_header(payload_, resp3::type::array, 2 + size * distance);
resp3::add_bulk(payload_, cmd);
resp3::add_bulk(payload_, key);
for (; begin != end; ++begin)
resp3::add_bulk(payload_, *begin);
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a dynamic number
* of arguments and don't have a key. For example
*
* \code
* std::set<std::string> channels
* { "channel1" , "channel2" , "channel3" }
*
* request req;
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
* \endcode
*
* \param cmd The Redis command
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
* \tparam ForwardIterator If the value type is not a std::string it will be converted to a string by calling `boost_redis_to_bulk`. This function must be made available over ADL and must have the following signature
*
* @code
* void boost_redis_to_bulk(std::string& to, T const& t);
* {
* boost::redis::resp3::boost_redis_to_bulk(to, serialize(t));
* }
* @endcode
*
* See cpp20_serialization.cpp
*/
template <class ForwardIterator>
void
push_range(
std::string_view const& cmd,
ForwardIterator begin,
ForwardIterator end,
typename std::iterator_traits<ForwardIterator>::value_type * = nullptr)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
if (begin == end)
return;
auto constexpr size = resp3::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
resp3::add_header(payload_, resp3::type::array, 1 + size * distance);
resp3::add_bulk(payload_, cmd);
for (; begin != end; ++begin)
resp3::add_bulk(payload_, *begin);
check_cmd(cmd);
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range of begin and end
* iterators.
*
* \param cmd Redis command.
* \param key Redis key.
* \param range Range to send e.g. `std::map`.
* \tparam A type that can be passed to `std::cbegin()` and `std::cend()`.
*/
template <class Range>
void
push_range(
std::string_view const& cmd,
std::string_view const& key,
Range const& range,
decltype(std::begin(range)) * = nullptr)
{
using std::begin;
using std::end;
push_range(cmd, key, begin(range), end(range));
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range of begin and end
* iterators.
*
* \param cmd Redis command.
* \param range Range to send e.g. `std::map`.
* \tparam A type that can be passed to `std::cbegin()` and `std::cend()`.
*/
template <class Range>
void
push_range(
std::string_view cmd,
Range const& range,
decltype(std::cbegin(range)) * = nullptr)
{
using std::cbegin;
using std::cend;
push_range(cmd, cbegin(range), cend(range));
}
private:
void check_cmd(std::string_view cmd)
{
if (!detail::has_response(cmd))
++commands_;
if (cmd == "HELLO")
has_hello_priority_ = cfg_.hello_with_priority;
}
config cfg_;
std::string payload_;
std::size_t commands_ = 0;
bool has_hello_priority_ = false;
};
} // boost::redis::resp3
#endif // BOOST_REDIS_REQUEST_HPP

View File

@@ -0,0 +1,209 @@
/* 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/resp3/parser.hpp>
#include <boost/redis/error.hpp>
#include <boost/assert.hpp>
#include <charconv>
namespace boost::redis::resp3 {
void to_int(int_type& i, std::string_view sv, system::error_code& ec)
{
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), i);
if (res.ec != std::errc())
ec = error::not_a_number;
}
parser::parser()
{
sizes_[0] = 2; // The sentinel must be more than 1.
}
std::size_t
parser::get_suggested_buffer_growth(std::size_t hint) const noexcept
{
if (!bulk_expected())
return hint;
if (hint < bulk_length_ + 2)
return bulk_length_ + 2;
return hint;
}
std::size_t
parser::get_consumed() const noexcept
{
return consumed_;
}
bool
parser::done() const noexcept
{
return depth_ == 0 && bulk_ == type::invalid && consumed_ != 0;
}
void
parser::commit_elem() noexcept
{
--sizes_[depth_];
while (sizes_[depth_] == 0) {
--depth_;
--sizes_[depth_];
}
}
auto
parser::consume(std::string_view view, system::error_code& ec) noexcept -> parser::result
{
switch (bulk_) {
case type::invalid:
{
auto const pos = view.find(sep, consumed_);
if (pos == std::string::npos)
return {}; // Needs more data to proceeed.
auto const t = to_type(view.at(consumed_));
auto const content = view.substr(consumed_ + 1, pos - 1 - consumed_);
auto const ret = consume_impl(t, content, ec);
if (ec)
return {};
consumed_ = pos + 2;
if (!bulk_expected())
return ret;
} [[fallthrough]];
default: // Handles bulk.
{
auto const span = bulk_length_ + 2;
if ((std::size(view) - consumed_) < span)
return {}; // Needs more data to proceeed.
auto const bulk_view = view.substr(consumed_, bulk_length_);
node_type const ret = {bulk_, 1, depth_, bulk_view};
bulk_ = type::invalid;
commit_elem();
consumed_ += span;
return ret;
}
}
}
auto
parser::consume_impl(
type t,
std::string_view elem,
system::error_code& ec) -> parser::node_type
{
BOOST_ASSERT(!bulk_expected());
node_type ret;
switch (t) {
case type::streamed_string_part:
{
to_int(bulk_length_ , elem, ec);
if (ec)
return {};
if (bulk_length_ == 0) {
ret = {type::streamed_string_part, 1, depth_, {}};
sizes_[depth_] = 1; // We are done.
bulk_ = type::invalid;
commit_elem();
} else {
bulk_ = type::streamed_string_part;
}
} break;
case type::blob_error:
case type::verbatim_string:
case type::blob_string:
{
if (elem.at(0) == '?') {
// NOTE: This can only be triggered with blob_string.
// Trick: A streamed string is read as an aggregate of
// infinite length. When the streaming is done the server
// is supposed to send a part with length 0.
sizes_[++depth_] = (std::numeric_limits<std::size_t>::max)();
ret = {type::streamed_string, 0, depth_, {}};
} else {
to_int(bulk_length_ , elem , ec);
if (ec)
return {};
bulk_ = t;
}
} break;
case type::boolean:
{
if (std::empty(elem)) {
ec = error::empty_field;
return {};
}
if (elem.at(0) != 'f' && elem.at(0) != 't') {
ec = error::unexpected_bool_value;
return {};
}
ret = {t, 1, depth_, elem};
commit_elem();
} break;
case type::doublean:
case type::big_number:
case type::number:
{
if (std::empty(elem)) {
ec = error::empty_field;
return {};
}
} [[fallthrough]];
case type::simple_error:
case type::simple_string:
case type::null:
{
ret = {t, 1, depth_, elem};
commit_elem();
} break;
case type::push:
case type::set:
case type::array:
case type::attribute:
case type::map:
{
int_type l = -1;
to_int(l, elem, ec);
if (ec)
return {};
ret = {t, l, depth_, {}};
if (l == 0) {
commit_elem();
} else {
if (depth_ == max_embedded_depth) {
ec = error::exceeeds_max_nested_depth;
return {};
}
++depth_;
sizes_[depth_] = l * element_multiplicity(t);
}
} break;
default:
{
ec = error::invalid_data_type;
return {};
}
}
return ret;
}
} // boost::redis::resp3

View File

@@ -0,0 +1,42 @@
/* 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/resp3/serialization.hpp>
#include <boost/redis/resp3/parser.hpp>
namespace boost::redis::resp3 {
void boost_redis_to_bulk(std::string& payload, std::string_view data)
{
auto const str = std::to_string(data.size());
payload += to_code(type::blob_string);
payload.append(std::cbegin(str), std::cend(str));
payload += parser::sep;
payload.append(std::cbegin(data), std::cend(data));
payload += parser::sep;
}
void add_header(std::string& payload, type t, std::size_t size)
{
auto const str = std::to_string(size);
payload += to_code(t);
payload.append(std::cbegin(str), std::cend(str));
payload += parser::sep;
}
void add_blob(std::string& payload, std::string_view blob)
{
payload.append(std::cbegin(blob), std::cend(blob));
payload += parser::sep;
}
void add_separator(std::string& payload)
{
payload += parser::sep;
}
} // boost::redis::resp3

View File

@@ -0,0 +1,42 @@
/* 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/resp3/type.hpp>
#include <boost/assert.hpp>
namespace boost::redis::resp3 {
auto to_string(type t) noexcept -> char const*
{
switch (t) {
case type::array: return "array";
case type::push: return "push";
case type::set: return "set";
case type::map: return "map";
case type::attribute: return "attribute";
case type::simple_string: return "simple_string";
case type::simple_error: return "simple_error";
case type::number: return "number";
case type::doublean: return "doublean";
case type::boolean: return "boolean";
case type::big_number: return "big_number";
case type::null: return "null";
case type::blob_error: return "blob_error";
case type::verbatim_string: return "verbatim_string";
case type::blob_string: return "blob_string";
case type::streamed_string: return "streamed_string";
case type::streamed_string_part: return "streamed_string_part";
default: return "invalid";
}
}
auto operator<<(std::ostream& os, type t) -> std::ostream&
{
os << to_string(t);
return os;
}
} // boost::redis::resp3

View File

@@ -4,46 +4,28 @@
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_RESP3_NODE_HPP
#define AEDIS_RESP3_NODE_HPP
#ifndef BOOST_REDIS_RESP3_NODE_HPP
#define BOOST_REDIS_RESP3_NODE_HPP
#include <aedis/resp3/type.hpp>
#include <boost/redis/resp3/type.hpp>
namespace aedis::resp3 {
namespace boost::redis::resp3 {
/** \brief A node in the response tree.
* \ingroup high-level-api
*
* RESP3 can contain recursive data structures: A map of sets of
* vector of etc. As it is parsed each element is passed to user
* callbacks (push parser), the `aedis::adapt` function. The signature of this
* callbacks (push parser). The signature of this
* callback is `f(resp3::node<std::string_view)`. This class is called a node
* because it can be seen as the element of the response tree. It
* is a template so that users can use it with owing strings e.g.
* `std::string` or `boost::static_string` etc. if they decide to use a node as
* response type, for example, to read a non-aggregate data-type use
* `std::string` or `boost::static_string` etc.
*
* ```cpp
* resp3::node<std::string> resp;
* co_await conn->async_exec(req, adapt(resp));
* ```
*
* for an aggregate use instead
*
* ```cpp
* std::vector<resp3::node<std::string>> resp; co_await
* conn->async_exec(req, adapt(resp));
* ```
*
* The vector will contain the
* [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR)
* view of the response tree. Any Redis response can be received in
* an array of nodes as shown above.
*
* \tparam String A `std::string`-like type.
* @tparam String A `std::string`-like type.
*/
template <class String>
struct node {
struct basic_node {
/// The RESP3 type of the data in this node.
type data_type = type::invalid;
@@ -58,13 +40,13 @@ struct node {
};
/** @brief Compares a node for equality.
* @relates node
* @relates basic_node
*
* @param a Left hand side node object.
* @param b Right hand side node object.
*/
template <class String>
auto operator==(node<String> const& a, node<String> const& b)
auto operator==(basic_node<String> const& a, basic_node<String> const& b)
{
return a.aggregate_size == b.aggregate_size
&& a.depth == b.depth
@@ -72,6 +54,11 @@ auto operator==(node<String> const& a, node<String> const& b)
&& a.value == b.value;
};
} // aedis::resp3
/** @brief A node in the response tree.
* @ingroup high-level-api
*/
using node = basic_node<std::string>;
#endif // AEDIS_RESP3_NODE_HPP
} // boost::redis::resp3
#endif // BOOST_REDIS_RESP3_NODE_HPP

View File

@@ -0,0 +1,102 @@
/* 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_RESP3_PARSER_HPP
#define BOOST_REDIS_RESP3_PARSER_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <limits>
#include <string_view>
#include <cstdint>
#include <optional>
namespace boost::redis::resp3 {
using int_type = std::uint64_t;
class parser {
public:
using node_type = basic_node<std::string_view>;
using result = std::optional<node_type>;
static constexpr std::size_t max_embedded_depth = 5;
static constexpr std::string_view sep = "\r\n";
private:
// The current depth. Simple data types will have depth 0, whereas
// the elements of aggregates will have depth 1. Embedded types
// will have increasing depth.
std::size_t depth_ = 0;
// The parser supports up to 5 levels of nested structures. The
// first element in the sizes stack is a sentinel and must be
// different from 1.
std::array<std::size_t, max_embedded_depth + 1> sizes_ = {{1}};
// Contains the length expected in the next bulk read.
int_type bulk_length_ = (std::numeric_limits<unsigned long>::max)();
// The type of the next bulk. Contains type::invalid if no bulk is
// expected.
type bulk_ = type::invalid;
// The number of bytes consumed from the buffer.
std::size_t consumed_ = 0;
// Returns the number of bytes that have been consumed.
auto consume_impl(type t, std::string_view elem, system::error_code& ec) -> node_type;
void commit_elem() noexcept;
// The bulk type expected in the next read. If none is expected
// returns type::invalid.
[[nodiscard]]
auto bulk_expected() const noexcept -> bool
{ return bulk_ != type::invalid; }
public:
parser();
// Returns true when the parser is done with the current message.
[[nodiscard]]
auto done() const noexcept -> bool;
auto get_suggested_buffer_growth(std::size_t hint) const noexcept -> std::size_t;
auto get_consumed() const noexcept -> std::size_t;
auto consume(std::string_view view, system::error_code& ec) noexcept -> result;
};
template <class Adapter>
bool
parse(
resp3::parser& p,
std::string_view const& msg,
Adapter& adapter,
system::error_code& ec)
{
while (!p.done()) {
auto const res = p.consume(msg, ec);
if (ec)
return true;
if (!res)
return false;
adapter(res.value(), ec);
if (ec)
return true;
}
return true;
}
} // boost::redis::resp3
#endif // BOOST_REDIS_RESP3_PARSER_HPP

View File

@@ -0,0 +1,144 @@
/* 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_RESP3_SERIALIZATION_HPP
#define BOOST_REDIS_RESP3_SERIALIZATION_HPP
#include <boost/redis/resp3/type.hpp>
#include <boost/system/system_error.hpp>
#include <boost/throw_exception.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <string>
#include <tuple>
// NOTE: Consider detecting tuples in the type in the parameter pack
// to calculate the header size correctly.
namespace boost::redis::resp3 {
/** @brief Adds a bulk to the request.
* @relates boost::redis::request
*
* This function is useful in serialization of your own data
* structures in a request. For example
*
* @code
* void boost_redis_to_bulk(std::string& payload, mystruct const& obj)
* {
* auto const str = // Convert obj to a string.
* boost_redis_to_bulk(payload, str);
* }
* @endcode
*
* @param payload Storage on which data will be copied into.
* @param data Data that will be serialized and stored in `payload`.
*
* See more in @ref serialization.
*/
void boost_redis_to_bulk(std::string& payload, std::string_view data);
template <class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
void boost_redis_to_bulk(std::string& payload, T n)
{
auto const s = std::to_string(n);
boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s});
}
template <class T>
struct add_bulk_impl {
static void add(std::string& payload, T const& from)
{
using namespace boost::redis::resp3;
boost_redis_to_bulk(payload, from);
}
};
template <class ...Ts>
struct add_bulk_impl<std::tuple<Ts...>> {
static void add(std::string& payload, std::tuple<Ts...> const& t)
{
auto f = [&](auto const&... vs)
{
using namespace boost::redis::resp3;
(boost_redis_to_bulk(payload, vs), ...);
};
std::apply(f, t);
}
};
template <class U, class V>
struct add_bulk_impl<std::pair<U, V>> {
static void add(std::string& payload, std::pair<U, V> const& from)
{
using namespace boost::redis::resp3;
boost_redis_to_bulk(payload, from.first);
boost_redis_to_bulk(payload, from.second);
}
};
void add_header(std::string& payload, type t, std::size_t size);
template <class T>
void add_bulk(std::string& payload, T const& data)
{
add_bulk_impl<T>::add(payload, data);
}
template <class>
struct bulk_counter;
template <class>
struct bulk_counter {
static constexpr auto size = 1U;
};
template <class T, class U>
struct bulk_counter<std::pair<T, U>> {
static constexpr auto size = 2U;
};
void add_blob(std::string& payload, std::string_view blob);
void add_separator(std::string& payload);
namespace detail
{
template <class Adapter>
void deserialize(std::string_view const& data, Adapter adapter, system::error_code& ec)
{
parser parser;
while (!parser.done()) {
auto const res = parser.consume(data, ec);
if (ec)
return;
BOOST_ASSERT(res.has_value());
adapter(res.value(), ec);
if (ec)
return;
}
BOOST_ASSERT(parser.get_consumed() == std::size(data));
}
template <class Adapter>
void deserialize(std::string_view const& data, Adapter adapter)
{
system::error_code ec;
deserialize(data, adapter, ec);
if (ec)
BOOST_THROW_EXCEPTION(system::system_error{ec});
}
}
} // boost::redis::resp3
#endif // BOOST_REDIS_RESP3_SERIALIZATION_HPP

View File

@@ -4,42 +4,76 @@
* accompanying file LICENSE.txt)
*/
#include <aedis/resp3/type.hpp>
#ifndef BOOST_REDIS_RESP3_TYPE_HPP
#define BOOST_REDIS_RESP3_TYPE_HPP
#include <boost/assert.hpp>
#include <ostream>
#include <vector>
#include <string>
namespace aedis::resp3 {
namespace boost::redis::resp3 {
auto to_string(type t) -> char const*
{
switch (t) {
case type::array: return "array";
case type::push: return "push";
case type::set: return "set";
case type::map: return "map";
case type::attribute: return "attribute";
case type::simple_string: return "simple_string";
case type::simple_error: return "simple_error";
case type::number: return "number";
case type::doublean: return "doublean";
case type::boolean: return "boolean";
case type::big_number: return "big_number";
case type::null: return "null";
case type::blob_error: return "blob_error";
case type::verbatim_string: return "verbatim_string";
case type::blob_string: return "blob_string";
case type::streamed_string: return "streamed_string";
case type::streamed_string_part: return "streamed_string_part";
default: return "invalid";
}
}
/** \brief RESP3 data types.
\ingroup high-level-api
The RESP3 specification can be found at https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md.
*/
enum class type
{ /// Aggregate
array,
/// Aaggregate
push,
/// Aggregate
set,
/// Aggregate
map,
/// Aggregate
attribute,
/// Simple
simple_string,
/// Simple
simple_error,
/// Simple
number,
/// Simple
doublean,
/// Simple
boolean,
/// Simple
big_number,
/// Simple
null,
/// Simple
blob_error,
/// Simple
verbatim_string,
/// Simple
blob_string,
/// Simple
streamed_string,
/// Simple
streamed_string_part,
/// Invalid
invalid
};
auto operator<<(std::ostream& os, type t) -> std::ostream&
{
os << to_string(t);
return os;
}
/** \brief Converts the data type to a string.
* \ingroup high-level-api
* \param t RESP3 type.
*/
auto to_string(type t) noexcept -> char const*;
auto is_aggregate(type t) -> bool
/** \brief Writes the type to the output stream.
* \ingroup high-level-api
* \param os Output stream.
* \param t RESP3 type.
*/
auto operator<<(std::ostream& os, type t) -> std::ostream&;
/* Checks whether the data type is an aggregate.
*/
constexpr auto is_aggregate(type t) noexcept -> bool
{
switch (t) {
case type::array:
@@ -51,7 +85,9 @@ auto is_aggregate(type t) -> bool
}
}
auto element_multiplicity(type t) -> std::size_t
// For map and attribute data types this function returns 2. All
// other types have value 1.
constexpr auto element_multiplicity(type t) noexcept -> std::size_t
{
switch (t) {
case type::map:
@@ -60,7 +96,8 @@ auto element_multiplicity(type t) -> std::size_t
}
}
auto to_code(type t) -> char
// Returns the wire code of a given type.
constexpr auto to_code(type t) noexcept -> char
{
switch (t) {
case type::blob_error: return '!';
@@ -84,7 +121,8 @@ auto to_code(type t) -> char
}
}
auto to_type(char c) -> type
// Converts a wire-format RESP3 type (char) to a resp3 type.
constexpr auto to_type(char c) noexcept -> type
{
switch (c) {
case '!': return type::blob_error;
@@ -107,4 +145,6 @@ auto to_type(char c) -> type
}
}
} // aedis::resp3
} // boost::redis::resp3
#endif // BOOST_REDIS_RESP3_TYPE_HPP

View File

@@ -0,0 +1,37 @@
/* 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_RESPONSE_HPP
#define BOOST_REDIS_RESPONSE_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/adapter/result.hpp>
#include <vector>
#include <string>
#include <tuple>
namespace boost::redis {
/** @brief Response with compile-time size.
* @ingroup high-level-api
*/
template <class... Ts>
using response = std::tuple<adapter::result<Ts>...>;
/** @brief A generic response to a request
* @ingroup high-level-api
*
* This response type can store any type of RESP3 data structure. It
* contains the
* [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR)
* view of the response tree.
*/
using generic_response = adapter::result<std::vector<resp3::node>>;
} // boost::redis::resp3
#endif // BOOST_REDIS_RESPONSE_HPP

View File

@@ -0,0 +1,14 @@
/* 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/impl/error.ipp>
#include <boost/redis/impl/logger.ipp>
#include <boost/redis/impl/request.ipp>
#include <boost/redis/impl/ignore.ipp>
#include <boost/redis/impl/connection.ipp>
#include <boost/redis/resp3/impl/type.ipp>
#include <boost/redis/resp3/impl/parser.ipp>
#include <boost/redis/resp3/impl/serialization.ipp>

54
tests/common.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "common.hpp"
#include <iostream>
#include <boost/asio/consign.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/test/unit_test.hpp>
namespace net = boost::asio;
struct run_callback {
std::shared_ptr<boost::redis::connection> conn;
boost::redis::operation op;
boost::system::error_code expected;
void operator()(boost::system::error_code const& ec) const
{
std::cout << "async_run: " << ec.message() << std::endl;
//BOOST_CHECK_EQUAL(ec, expected);
conn->cancel(op);
}
};
void
run(
std::shared_ptr<boost::redis::connection> conn,
boost::redis::config cfg,
boost::system::error_code ec,
boost::redis::operation op,
boost::redis::logger::level l)
{
conn->async_run(cfg, {l}, run_callback{conn, op, ec});
}
#ifdef BOOST_ASIO_HAS_CO_AWAIT
auto start(net::awaitable<void> op) -> int
{
try {
net::io_context ioc;
net::co_spawn(ioc, std::move(op), [](std::exception_ptr p) {
if (p)
std::rethrow_exception(p);
});
ioc.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "start> " << e.what() << std::endl;
}
return 1;
}
#endif // BOOST_ASIO_HAS_CO_AWAIT

View File

@@ -1,23 +1,25 @@
#pragma once
#include <boost/asio.hpp>
#include <chrono>
namespace net = boost::asio;
using endpoints = net::ip::tcp::resolver::results_type;
auto
resolve(
std::string const& host = "127.0.0.1",
std::string const& port = "6379") -> endpoints
{
net::io_context ioc;
net::ip::tcp::resolver resv{ioc};
return resv.resolve(host, port);
}
#include <boost/system/error_code.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/redis/connection.hpp>
#include <boost/redis/operation.hpp>
#include <memory>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
inline
auto redir(boost::system::error_code& ec)
{ return net::redirect_error(net::use_awaitable, ec); }
{ return boost::asio::redirect_error(boost::asio::use_awaitable, ec); }
auto start(boost::asio::awaitable<void> op) -> int;
#endif // BOOST_ASIO_HAS_CO_AWAIT
void
run(
std::shared_ptr<boost::redis::connection> conn,
boost::redis::config cfg = {},
boost::system::error_code ec = boost::asio::error::operation_aborted,
boost::redis::operation op = boost::redis::operation::receive,
boost::redis::logger::level l = boost::redis::logger::level::info);

View File

@@ -1,86 +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 <iostream>
#include <boost/asio.hpp>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
#include "../examples/common/common.hpp"
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using error_code = boost::system::error_code;
using aedis::operation;
using aedis::adapt;
auto push_consumer(std::shared_ptr<connection> conn, int expected) -> net::awaitable<void>
{
int c = 0;
for (;;) {
co_await conn->async_receive(adapt(), net::use_awaitable);
if (++c == expected)
break;
}
resp3::request req;
req.push("HELLO", 3);
req.push("QUIT");
co_await conn->async_exec(req, adapt());
}
auto echo_session(std::shared_ptr<connection> conn, std::string id, int n) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
resp3::request req;
std::tuple<aedis::ignore, std::string> resp;
for (auto i = 0; i < n; ++i) {
auto const msg = id + "/" + std::to_string(i);
//std::cout << msg << std::endl;
req.push("HELLO", 3);
req.push("PING", msg);
req.push("SUBSCRIBE", "channel");
boost::system::error_code ec;
co_await conn->async_exec(req, adapt(resp), redir(ec));
BOOST_CHECK_EQUAL(ec, boost::system::error_code{});
BOOST_CHECK_EQUAL(msg, std::get<1>(resp));
req.clear();
std::get<1>(resp).clear();
}
}
auto async_echo_stress() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
int const sessions = 500;
int const msgs = 1000;
int total = sessions * msgs;
net::co_spawn(ex, push_consumer(conn, total), net::detached);
for (int i = 0; i < sessions; ++i)
net::co_spawn(ex, echo_session(conn, std::to_string(i), msgs), net::detached);
co_await connect(conn, "127.0.0.1", "6379");
co_await conn->async_run();
}
BOOST_AUTO_TEST_CASE(echo_stress)
{
run(async_echo_stress());
}
#else
int main(){}
#endif

View File

@@ -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)
*/
#include <iostream>
#include <boost/asio.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
// TODO: Test whether HELLO won't be inserted passt commands that have
// been already writen.
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using error_code = boost::system::error_code;
using connection = aedis::connection;
using aedis::adapt;
BOOST_AUTO_TEST_CASE(hello_priority)
{
resp3::request req1;
req1.get_config().coalesce = false;
req1.push("PING", "req1");
resp3::request req2;
req2.get_config().coalesce = false;
req2.get_config().hello_with_priority = false;
req2.push("HELLO", 3);
req2.push("PING", "req2");
req2.push("QUIT");
resp3::request req3;
req3.get_config().coalesce = false;
req3.get_config().hello_with_priority = true;
req3.push("HELLO", 3);
req3.push("PING", "req3");
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
bool seen1 = false;
bool seen2 = false;
bool seen3 = false;
conn.async_exec(req1, adapt(), [&](auto ec, auto){
std::cout << "bbb" << std::endl;
BOOST_TEST(!ec);
BOOST_TEST(!seen2);
BOOST_TEST(seen3);
seen1 = true;
});
conn.async_exec(req2, adapt(), [&](auto ec, auto){
std::cout << "ccc" << std::endl;
BOOST_TEST(!ec);
BOOST_TEST(seen1);
BOOST_TEST(seen3);
seen2 = true;
});
conn.async_exec(req3, adapt(), [&](auto ec, auto){
std::cout << "ddd" << std::endl;
BOOST_TEST(!ec);
BOOST_TEST(!seen1);
BOOST_TEST(!seen2);
seen3 = true;
});
conn.async_run([](auto ec){
BOOST_TEST(!ec);
});
ioc.run();
}
BOOST_AUTO_TEST_CASE(wrong_response_data_type)
{
resp3::request req;
req.push("HELLO", 3);
req.push("QUIT");
// Wrong data type.
std::tuple<aedis::ignore, int> resp;
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
conn.async_exec(req, adapt(resp), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, aedis::error::not_a_number);
});
conn.async_run([](auto ec){
BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted);
});
ioc.run();
}
BOOST_AUTO_TEST_CASE(cancel_request_if_not_connected)
{
resp3::request req;
req.get_config().cancel_if_not_connected = true;
req.push("HELLO", 3);
req.push("PING");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
conn->async_exec(req, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, aedis::error::not_connected);
});
ioc.run();
}

View File

@@ -1,180 +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 <iostream>
#include <boost/asio.hpp>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/system/errc.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
#include "../examples/common/common.hpp"
// NOTE1: Sends hello separately. I have observed that if hello and
// blpop are sent toguether, Redis will send the response of hello
// right away, not waiting for blpop. That is why we have to send it
// separately here.
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using error_code = boost::system::error_code;
using namespace net::experimental::awaitable_operators;
using aedis::operation;
using aedis::adapt;
auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
co_await connect(conn, "127.0.0.1", "6379");
conn->async_run([conn](auto ec) {
BOOST_TEST(!ec);
});
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
// See NOTE1.
resp3::request req0;
req0.get_config().coalesce = false;
req0.push("HELLO", 3);
std::ignore = co_await conn->async_exec(req0, adapt(), net::use_awaitable);
resp3::request req1;
req1.get_config().coalesce = false;
req1.push("BLPOP", "any", 3);
// Should not be canceled.
conn->async_exec(req1, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
resp3::request req2;
req2.get_config().coalesce = false;
req2.push("PING", "second");
// Should be canceled.
conn->async_exec(req2, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted);
});
// Will complete while BLPOP is pending.
boost::system::error_code ec1;
co_await st.async_wait(net::redirect_error(net::use_awaitable, ec1));
conn->cancel(operation::exec);
BOOST_TEST(!ec1);
resp3::request req3;
req3.push("QUIT");
// Test whether the connection remains usable after a call to
// cancel(exec).
co_await conn->async_exec(req3, adapt(), net::redirect_error(net::use_awaitable, ec1));
BOOST_TEST(!ec1);
}
auto ignore_implicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
co_await connect(conn, "127.0.0.1", "6379");
// Calls async_run separately from the group of ops below to avoid
// having it canceled when the timer fires.
conn->async_run([conn](auto ec) {
BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted);
});
// See NOTE1.
resp3::request req0;
req0.get_config().coalesce = false;
req0.push("HELLO", 3);
std::ignore = co_await conn->async_exec(req0, adapt(), net::use_awaitable);
// Will be cancelled after it has been written but before the
// response arrives.
resp3::request req1;
req1.get_config().coalesce = false;
req1.push("BLPOP", "any", 3);
// Will be cancelled before it is written.
resp3::request req2;
req2.get_config().coalesce = false;
req2.get_config().cancel_on_connection_lost = true;
req2.push("PING");
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
boost::system::error_code ec1, ec2, ec3;
co_await (
conn->async_exec(req1, adapt(), redir(ec1)) ||
conn->async_exec(req2, adapt(), redir(ec2)) ||
st.async_wait(redir(ec3))
);
BOOST_CHECK_EQUAL(ec1, net::error::basic_errors::operation_aborted);
BOOST_CHECK_EQUAL(ec2, net::error::basic_errors::operation_aborted);
BOOST_TEST(!ec3);
}
auto cancel_of_req_written_on_run_canceled() -> net::awaitable<void>
{
resp3::request req0;
req0.get_config().coalesce = false;
req0.push("HELLO", 3);
resp3::request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.get_config().cancel_if_unresponded = true;
req1.push("BLPOP", "any", 0);
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
co_await connect(conn, "127.0.0.1", "6379");
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
boost::system::error_code ec0, ec1, ec2, ec3;
co_await (
conn->async_exec(req0, adapt(), redir(ec0)) &&
(conn->async_exec(req1, adapt(), redir(ec1)) ||
conn->async_run(redir(ec2)) ||
st.async_wait(redir(ec3)))
);
BOOST_TEST(!ec0);
BOOST_CHECK_EQUAL(ec1, net::error::basic_errors::operation_aborted);
BOOST_CHECK_EQUAL(ec2, net::error::basic_errors::operation_aborted);
BOOST_TEST(!ec3);
}
BOOST_AUTO_TEST_CASE(test_ignore_explicit_cancel_of_req_written)
{
run(async_ignore_explicit_cancel_of_req_written());
}
BOOST_AUTO_TEST_CASE(test_ignore_implicit_cancel_of_req_written)
{
run(ignore_implicit_cancel_of_req_written());
}
BOOST_AUTO_TEST_CASE(test_cancel_of_req_written_on_run_canceled)
{
run(cancel_of_req_written_on_run_canceled());
}
#else
int main(){}
#endif

View File

@@ -1,327 +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 <iostream>
#include <boost/asio.hpp>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/system/errc.hpp>
#include <boost/asio/experimental/as_tuple.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
namespace net = boost::asio;
using aedis::resp3::request;
using aedis::adapt;
using aedis::operation;
using connection = aedis::connection;
using error_code = boost::system::error_code;
using net::experimental::as_tuple;
BOOST_AUTO_TEST_CASE(push_filtered_out)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
request req;
req.push("HELLO", 3);
req.push("PING");
req.push("SUBSCRIBE", "channel");
req.push("QUIT");
std::tuple<aedis::ignore, std::string, std::string> resp;
conn.async_exec(req, adapt(resp), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_receive(adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_run([](auto ec){
BOOST_TEST(!ec);
});
ioc.run();
BOOST_CHECK_EQUAL(std::get<1>(resp), "PONG");
BOOST_CHECK_EQUAL(std::get<2>(resp), "OK");
}
void receive_wrong_syntax(request const& req)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
conn.async_exec(req, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_run([](auto ec){
BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted);
});
conn.async_receive(adapt(), [&](auto ec, auto){
BOOST_TEST(!ec);
conn.cancel(aedis::operation::run);
});
ioc.run();
}
#ifdef BOOST_ASIO_HAS_CO_AWAIT
net::awaitable<void> push_consumer1(connection& conn, bool& push_received)
{
{
auto [ec, ev] = co_await conn.async_receive(adapt(), as_tuple(net::use_awaitable));
BOOST_TEST(!ec);
}
{
auto [ec, ev] = co_await conn.async_receive(adapt(), as_tuple(net::use_awaitable));
BOOST_CHECK_EQUAL(ec, net::experimental::channel_errc::channel_cancelled);
}
push_received = true;
}
struct adapter_error {
void
operator()(
std::size_t, aedis::resp3::node<std::string_view> const&, boost::system::error_code& ec)
{
ec = aedis::error::incompatible_size;
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return static_cast<std::size_t>(-1);}
[[nodiscard]]
auto get_max_read_size(std::size_t) const noexcept
{ return static_cast<std::size_t>(-1);}
};
BOOST_AUTO_TEST_CASE(test_push_adapter)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
request req;
req.push("HELLO", 3);
req.push("PING");
req.push("SUBSCRIBE", "channel");
req.push("PING");
conn.async_receive(adapter_error{}, [](auto ec, auto) {
BOOST_CHECK_EQUAL(ec, aedis::error::incompatible_size);
});
conn.async_exec(req, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, net::experimental::error::channel_errors::channel_cancelled);
});
conn.async_run([](auto ec){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
ioc.run();
// TODO: Reset the ioc reconnect and send a quit to ensure
// reconnection is possible after an error.
}
void test_push_is_received1(bool coalesce)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
request req{{false, coalesce}};
req.push("HELLO", 3);
req.push("SUBSCRIBE", "channel");
req.push("QUIT");
conn.async_exec(req, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_run([&](auto ec){
BOOST_TEST(!ec);
conn.cancel(operation::receive);
});
bool push_received = false;
net::co_spawn(
ioc.get_executor(),
push_consumer1(conn, push_received),
net::detached);
ioc.run();
BOOST_TEST(push_received);
}
void test_push_is_received2(bool coalesce)
{
request req1{{false, coalesce}};
req1.push("HELLO", 3);
req1.push("PING", "Message1");
request req2{{false, coalesce}};
req2.push("SUBSCRIBE", "channel");
request req3{{false, coalesce}};
req3.push("PING", "Message2");
req3.push("QUIT");
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
auto handler =[](auto ec, auto...)
{
BOOST_TEST(!ec);
};
conn.async_exec(req1, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req3, adapt(), handler);
conn.async_run([&](auto ec) {
BOOST_TEST(!ec);
conn.cancel(operation::receive);
});
bool push_received = false;
net::co_spawn(
ioc.get_executor(),
push_consumer1(conn, push_received),
net::detached);
ioc.run();
BOOST_TEST(push_received);
}
net::awaitable<void> push_consumer3(connection& conn)
{
for (;;)
co_await conn.async_receive(adapt(), net::use_awaitable);
}
// Test many subscribe requests.
void test_push_many_subscribes(bool coalesce)
{
request req0{{false, coalesce}};
req0.push("HELLO", 3);
request req1{{false, coalesce}};
req1.push("PING", "Message1");
request req2{{false, coalesce}};
req2.push("SUBSCRIBE", "channel");
request req3{{false, coalesce}};
req3.push("QUIT");
auto handler =[](auto ec, auto...)
{
BOOST_TEST(!ec);
};
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
conn.async_exec(req0, adapt(), handler);
conn.async_exec(req1, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req1, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req1, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req1, adapt(), handler);
conn.async_exec(req2, adapt(), handler);
conn.async_exec(req3, adapt(), handler);
conn.async_run([&](auto ec) {
BOOST_TEST(!ec);
conn.cancel(operation::receive);
});
net::co_spawn(ioc.get_executor(), push_consumer3(conn), net::detached);
ioc.run();
}
BOOST_AUTO_TEST_CASE(push_received1)
{
test_push_is_received1(true);
test_push_is_received1(false);
}
BOOST_AUTO_TEST_CASE(push_received2)
{
test_push_is_received2(true);
test_push_is_received2(false);
}
BOOST_AUTO_TEST_CASE(many_subscribers)
{
test_push_many_subscribes(true);
test_push_many_subscribes(false);
}
#endif
BOOST_AUTO_TEST_CASE(receive_wrong_syntax1)
{
request req1{{false}};
req1.push("HELLO", 3);
req1.push("PING", "Message");
req1.push("SUBSCRIBE"); // Wrong command synthax.
req1.get_config().coalesce = true;
receive_wrong_syntax(req1);
req1.get_config().coalesce = false;
receive_wrong_syntax(req1);
}
BOOST_AUTO_TEST_CASE(receice_wrong_syntay2)
{
request req2{{false}};
req2.push("HELLO", 3);
req2.push("SUBSCRIBE"); // Wrong command syntax.
req2.get_config().coalesce = true;
receive_wrong_syntax(req2);
req2.get_config().coalesce = false;
receive_wrong_syntax(req2);
}
#else
int main() {}
#endif

View File

@@ -1,96 +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 <iostream>
#include <boost/asio.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::connection;
using error_code = boost::system::error_code;
using operation = aedis::operation;
// Test if quit causes async_run to exit.
BOOST_AUTO_TEST_CASE(test_quit_no_coalesce)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
request req1;
req1.get_config().cancel_on_connection_lost = false;
req1.get_config().coalesce = false;
req1.push("PING");
request req2;
req2.get_config().cancel_on_connection_lost = false;
req2.get_config().coalesce = false;
req2.push("QUIT");
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_exec(req2, adapt(), [](auto ec, auto) {
BOOST_TEST(!ec);
});
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_run([&](auto ec){
BOOST_TEST(!ec);
conn.cancel(operation::exec);
});
ioc.run();
}
void test_quit2(bool coalesce)
{
request req{{false, coalesce}};
req.push("HELLO", 3);
req.push("QUIT");
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
conn.async_exec(req, adapt(), [](auto ec, auto) {
BOOST_TEST(!ec);
});
conn.async_run([](auto ec) {
BOOST_TEST(!ec);
});
ioc.run();
}
BOOST_AUTO_TEST_CASE(test_quit)
{
test_quit2(true);
test_quit2(false);
}

View File

@@ -1,58 +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 <iostream>
#include <boost/asio.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::connection;
using error_code = boost::system::error_code;
using operation = aedis::operation;
BOOST_AUTO_TEST_CASE(test_quit_coalesce)
{
net::io_context ioc;
auto const endpoints = resolve();
connection conn{ioc};
net::connect(conn.next_layer(), endpoints);
request req1{{false, true}};
req1.push("PING");
request req2{{false, true}};
req2.push("QUIT");
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_exec(req2, adapt(), [](auto ec, auto){
BOOST_TEST(!ec);
});
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, net::error::misc_errors::eof);
});
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_run([&](auto ec){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
conn.cancel(operation::exec);
});
ioc.run();
}

View File

@@ -1,72 +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 <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/ssl/connection.hpp>
#include <aedis/src.hpp>
#include "common.hpp"
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::ssl::connection;
struct endpoint {
std::string host;
std::string port;
};
bool verify_certificate(bool, net::ssl::verify_context&)
{
std::cout << "set_verify_callback" << std::endl;
return true;
}
BOOST_AUTO_TEST_CASE(ping)
{
std::string const in = "Kabuf";
request req;
req.get_config().cancel_on_connection_lost = true;
req.push("HELLO", 3, "AUTH", "aedis", "aedis");
req.push("PING", in);
req.push("QUIT");
std::string out;
auto resp = std::tie(std::ignore, out, std::ignore);
auto const endpoints = resolve("db.occase.de", "6380");
net::io_context ioc;
net::ssl::context ctx{net::ssl::context::sslv23};
connection conn{ioc, ctx};
conn.next_layer().set_verify_mode(net::ssl::verify_peer);
conn.next_layer().set_verify_callback(verify_certificate);
net::connect(conn.lowest_layer(), endpoints);
conn.next_layer().handshake(net::ssl::stream_base::client);
conn.async_exec(req, adapt(resp), [](auto ec, auto) {
BOOST_TEST(!ec);
});
conn.async_run([](auto ec) {
BOOST_TEST(!ec);
});
ioc.run();
BOOST_CHECK_EQUAL(in, out);
}

View File

@@ -1,650 +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 <map>
#include <iostream>
#include <optional>
#include <sstream>
#include <boost/system/errc.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/beast/_experimental/test/stream.hpp>
#define BOOST_TEST_MODULE low level
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
// TODO: Test with empty strings.
namespace std
{
auto operator==(aedis::ignore, aedis::ignore) noexcept {return true;}
auto operator!=(aedis::ignore, aedis::ignore) noexcept {return false;}
}
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using test_stream = boost::beast::test::stream;
using aedis::adapter::adapt2;
using node_type = aedis::resp3::node<std::string>;
using vec_node_type = std::vector<node_type>;
using vec_type = std::vector<std::string>;
using op_vec_type = std::optional<std::vector<std::string>>;
// Set
using set_type = std::set<std::string>;
using mset_type = std::multiset<std::string>;
using uset_type = std::unordered_set<std::string>;
using muset_type = std::unordered_multiset<std::string>;
// Array
using tuple_int_2 = std::tuple<int, int>;
using array_type = std::array<int, 3>;
using array_type2 = std::array<int, 1>;
// Map
using map_type = std::map<std::string, std::string>;
using mmap_type = std::multimap<std::string, std::string>;
using umap_type = std::unordered_map<std::string, std::string>;
using mumap_type = std::unordered_multimap<std::string, std::string>;
using op_map_type = std::optional<std::map<std::string, std::string>>;
using tuple8_type = std::tuple<std::string, std::string, std::string, std::string, std::string, std::string, std::string, std::string>;
// Null
using op_type_01 = std::optional<bool>;
using op_type_02 = std::optional<int>;
using op_type_03 = std::optional<std::string>;
using op_type_04 = std::optional<std::vector<std::string>>;
using op_type_05 = std::optional<std::list<std::string>>;
using op_type_06 = std::optional<std::map<std::string, std::string>>;
using op_type_07 = std::optional<std::unordered_map<std::string, std::string>>;
using op_type_08 = std::optional<std::set<std::string>>;
using op_type_09 = std::optional<std::unordered_set<std::string>>;
//-------------------------------------------------------------------
template <class Result>
struct expect {
std::string in;
Result expected;
boost::system::error_code ec{};
};
template <class Result>
auto make_expected(std::string in, Result expected, boost::system::error_code ec = {})
{
return expect<Result>{in, expected, ec};
}
template <class Result>
void test_sync(net::any_io_executor ex, expect<Result> e)
{
std::string rbuffer;
test_stream ts {ex};
ts.append(e.in);
Result result;
boost::system::error_code ec;
resp3::read(ts, net::dynamic_buffer(rbuffer), adapt2(result), ec);
BOOST_CHECK_EQUAL(ec, e.ec);
if (e.ec)
return;
BOOST_TEST(rbuffer.empty());
auto const res = result == e.expected;
BOOST_TEST(res);
}
template <class Result>
class async_test: public std::enable_shared_from_this<async_test<Result>> {
private:
std::string rbuffer_;
test_stream ts_;
expect<Result> data_;
Result result_;
public:
async_test(net::any_io_executor ex, expect<Result> e)
: ts_{ex}
, data_{e}
{
ts_.append(e.in);
}
void run()
{
auto self = this->shared_from_this();
auto f = [self](auto ec, auto)
{
BOOST_CHECK_EQUAL(ec, self->data_.ec);
if (self->data_.ec)
return;
BOOST_TEST(self->rbuffer_.empty());
auto const res = self->result_ == self->data_.expected;
BOOST_TEST(res);
};
resp3::async_read(
ts_,
net::dynamic_buffer(rbuffer_),
adapt2(result_),
f);
}
};
template <class Result>
void test_async(net::any_io_executor ex, expect<Result> e)
{
std::make_shared<async_test<Result>>(ex, e)->run();
}
auto make_blob()
{
std::string str(100000, 'a');
str[1000] = '\r';
str[1001] = '\n';
return str;
return str;
}
auto const blob = make_blob();
auto make_blob_string(std::string const& b)
{
std::string wire;
wire += '$';
wire += std::to_string(b.size());
wire += "\r\n";
wire += b;
wire += "\r\n";
return wire;
}
std::optional<int> op_int_ok = 11;
std::optional<bool> op_bool_ok = true;
// TODO: Test a streamed string that is not finished with a string of
// size 0 but other command comes in.
std::vector<node_type> streamed_string_e1
{ {aedis::resp3::type::streamed_string, 0, 1, ""}
, {aedis::resp3::type::streamed_string_part, 1, 1, "Hell"}
, {aedis::resp3::type::streamed_string_part, 1, 1, "o wor"}
, {aedis::resp3::type::streamed_string_part, 1, 1, "d"}
, {aedis::resp3::type::streamed_string_part, 1, 1, ""}
};
std::vector<node_type> streamed_string_e2 { {resp3::type::streamed_string, 0UL, 1UL, {}}, {resp3::type::streamed_string_part, 1UL, 1UL, {}} };
std::vector<node_type> const push_e1a
{ {resp3::type::push, 4UL, 0UL, {}}
, {resp3::type::simple_string, 1UL, 1UL, "pubsub"}
, {resp3::type::simple_string, 1UL, 1UL, "message"}
, {resp3::type::simple_string, 1UL, 1UL, "some-channel"}
, {resp3::type::simple_string, 1UL, 1UL, "some message"}
};
std::vector<node_type> const push_e1b
{ {resp3::type::push, 0UL, 0UL, {}} };
std::vector<node_type> const set_expected1a
{ {resp3::type::set, 6UL, 0UL, {}}
, {resp3::type::simple_string, 1UL, 1UL, {"orange"}}
, {resp3::type::simple_string, 1UL, 1UL, {"apple"}}
, {resp3::type::simple_string, 1UL, 1UL, {"one"}}
, {resp3::type::simple_string, 1UL, 1UL, {"two"}}
, {resp3::type::simple_string, 1UL, 1UL, {"three"}}
, {resp3::type::simple_string, 1UL, 1UL, {"orange"}}
};
mset_type const set_e1f{"apple", "one", "orange", "orange", "three", "two"};
uset_type const set_e1c{"apple", "one", "orange", "three", "two"};
muset_type const set_e1g{"apple", "one", "orange", "orange", "three", "two"};
vec_type const set_e1d = {"orange", "apple", "one", "two", "three", "orange"};
op_vec_type const set_expected_1e = set_e1d;
std::vector<node_type> const array_e1a
{ {resp3::type::array, 3UL, 0UL, {}}
, {resp3::type::blob_string, 1UL, 1UL, {"11"}}
, {resp3::type::blob_string, 1UL, 1UL, {"22"}}
, {resp3::type::blob_string, 1UL, 1UL, {"3"}}
};
std::vector<int> const array_e1b{11, 22, 3};
std::vector<std::string> const array_e1c{"11", "22", "3"};
std::vector<std::string> const array_e1d{};
std::vector<node_type> const array_e1e{{resp3::type::array, 0UL, 0UL, {}}};
array_type const array_e1f{11, 22, 3};
std::list<int> const array_e1g{11, 22, 3};
std::deque<int> const array_e1h{11, 22, 3};
std::vector<node_type> const map_expected_1a
{ {resp3::type::map, 4UL, 0UL, {}}
, {resp3::type::blob_string, 1UL, 1UL, {"key1"}}
, {resp3::type::blob_string, 1UL, 1UL, {"value1"}}
, {resp3::type::blob_string, 1UL, 1UL, {"key2"}}
, {resp3::type::blob_string, 1UL, 1UL, {"value2"}}
, {resp3::type::blob_string, 1UL, 1UL, {"key3"}}
, {resp3::type::blob_string, 1UL, 1UL, {"value3"}}
, {resp3::type::blob_string, 1UL, 1UL, {"key3"}}
, {resp3::type::blob_string, 1UL, 1UL, {"value3"}}
};
map_type const map_expected_1b
{ {"key1", "value1"}
, {"key2", "value2"}
, {"key3", "value3"}
};
umap_type const map_e1g
{ {"key1", "value1"}
, {"key2", "value2"}
, {"key3", "value3"}
};
mmap_type const map_e1k
{ {"key1", "value1"}
, {"key2", "value2"}
, {"key3", "value3"}
, {"key3", "value3"}
};
mumap_type const map_e1l
{ {"key1", "value1"}
, {"key2", "value2"}
, {"key3", "value3"}
, {"key3", "value3"}
};
std::vector<std::string> const map_expected_1c
{ "key1", "value1"
, "key2", "value2"
, "key3", "value3"
, "key3", "value3"
};
op_map_type const map_expected_1d = map_expected_1b;
op_vec_type const map_expected_1e = map_expected_1c;
tuple8_type const map_e1f
{ std::string{"key1"}, std::string{"value1"}
, std::string{"key2"}, std::string{"value2"}
, std::string{"key3"}, std::string{"value3"}
, std::string{"key3"}, std::string{"value3"}
};
std::vector<node_type> const attr_e1a
{ {resp3::type::attribute, 1UL, 0UL, {}}
, {resp3::type::simple_string, 1UL, 1UL, "key-popularity"}
, {resp3::type::map, 2UL, 1UL, {}}
, {resp3::type::blob_string, 1UL, 2UL, "a"}
, {resp3::type::doublean, 1UL, 2UL, "0.1923"}
, {resp3::type::blob_string, 1UL, 2UL, "b"}
, {resp3::type::doublean, 1UL, 2UL, "0.0012"}
};
std::vector<node_type> const attr_e1b
{ {resp3::type::attribute, 0UL, 0UL, {}} };
#define S01a "#11\r\n"
#define S01b "#f\r\n"
#define S01c "#t\r\n"
#define S01d "#\r\n"
#define S02a "$?\r\n;0\r\n"
#define S02b "$?\r\n;4\r\nHell\r\n;5\r\no wor\r\n;1\r\nd\r\n;0\r\n"
#define S02c "$?\r\n;b\r\nHell\r\n;5\r\no wor\r\n;1\r\nd\r\n;0\r\n"
#define S02d "$?\r\n;d\r\nHell\r\n;5\r\no wor\r\n;1\r\nd\r\n;0\r\n"
#define S03a "%11\r\n"
#define S03b "%4\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n$4\r\nkey3\r\n$6\r\nvalue3\r\n$4\r\nkey3\r\n$6\r\nvalue3\r\n"
#define S03c "%0\r\n"
#define S03d "%rt\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
#define S04a "*1\r\n:11\r\n"
#define S04b "*3\r\n$2\r\n11\r\n$2\r\n22\r\n$1\r\n3\r\n"
#define S04c "*1\r\n" S03b
#define S04d "*1\r\n" S09a
#define S04e "*3\r\n$2\r\n11\r\n$2\r\n22\r\n$1\r\n3\r\n"
#define S04f "*1\r\n*1\r\n$2\r\nab\r\n"
#define S04g "*1\r\n*1\r\n*1\r\n*1\r\n*1\r\n*1\r\na\r\n"
#define S04h "*0\r\n"
#define S04i "*3\r\n$2\r\n11\r\n$2\r\n22\r\n$1\r\n3\r\n"
#define S05a ":-3\r\n"
#define S05b ":11\r\n"
#define s05c ":3\r\n"
#define S05d ":adf\r\n"
#define S05e ":\r\n"
#define S06a "_\r\n"
#define S07a ">4\r\n+pubsub\r\n+message\r\n+some-channel\r\n+some message\r\n"
#define S07b ">0\r\n"
#define S08a "|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n"
#define S08b "|0\r\n"
#define S09a "~6\r\n+orange\r\n+apple\r\n+one\r\n+two\r\n+three\r\n+orange\r\n"
#define S09b "~0\r\n"
#define S10a "-Error\r\n"
#define S10b "-\r\n"
#define S11a ",1.23\r\n"
#define S11b ",inf\r\n"
#define S11c ",-inf\r\n"
#define S11d ",1.23\r\n"
#define S11e ",er\r\n"
#define S11f ",\r\n"
#define S12a "!21\r\nSYNTAX invalid syntax\r\n"
#define S12b "!0\r\n\r\n"
#define S12c "!3\r\nfoo\r\n"
#define S13a "=15\r\ntxt:Some string\r\n"
#define S13b "=0\r\n\r\n"
#define S14a "(3492890328409238509324850943850943825024385\r\n"
#define S14b "(\r\n"
#define S15a "+OK\r\n"
#define S15b "+\r\n"
#define S16a "s11\r\n"
#define S17a "$l\r\nhh\r\n"
#define S17b "$2\r\nhh\r\n"
#define S18c "$26\r\nhhaa\aaaa\raaaaa\r\naaaaaaaaaa\r\n"
#define S18d "$0\r\n\r\n"
#define NUMBER_TEST_CONDITIONS(test) \
test(ex, make_expected(S01a, std::optional<bool>{}, aedis::error::unexpected_bool_value)); \
test(ex, make_expected(S01b, bool{false})); \
test(ex, make_expected(S01b, node_type{resp3::type::boolean, 1UL, 0UL, {"f"}})); \
test(ex, make_expected(S01c, bool{true})); \
test(ex, make_expected(S01c, node_type{resp3::type::boolean, 1UL, 0UL, {"t"}})); \
test(ex, make_expected(S01c, op_bool_ok)); \
test(ex, make_expected(S01c, std::map<int, int>{}, aedis::error::expects_resp3_map)); \
test(ex, make_expected(S01c, std::set<int>{}, aedis::error::expects_resp3_set)); \
test(ex, make_expected(S01c, std::unordered_map<int, int>{}, aedis::error::expects_resp3_map)); \
test(ex, make_expected(S01c, std::unordered_set<int>{}, aedis::error::expects_resp3_set)); \
test(ex, make_expected(S02a, streamed_string_e2)); \
test(ex, make_expected(S03a, int{}, aedis::error::expects_resp3_simple_type));\
test(ex, make_expected(S03a, std::optional<int>{}, aedis::error::expects_resp3_simple_type));; \
test(ex, make_expected(S02b, int{}, aedis::error::not_a_number)); \
test(ex, make_expected(S02b, std::string{"Hello word"})); \
test(ex, make_expected(S02b, streamed_string_e1)); \
test(ex, make_expected(S02c, std::string{}, aedis::error::not_a_number)); \
test(ex, make_expected(S04a, std::tuple<int>{11})); \
test(ex, make_expected(S05a, node_type{resp3::type::number, 1UL, 0UL, {"-3"}})); \
test(ex, make_expected(S05b, int{11})); \
test(ex, make_expected(S05b, op_int_ok)); \
test(ex, make_expected(S05b, std::list<std::string>{}, aedis::error::expects_resp3_aggregate)); \
test(ex, make_expected(S05b, std::map<std::string, std::string>{}, aedis::error::expects_resp3_map)); \
test(ex, make_expected(S05b, std::set<std::string>{}, aedis::error::expects_resp3_set)); \
test(ex, make_expected(S05b, std::unordered_map<std::string, std::string>{}, aedis::error::expects_resp3_map)); \
test(ex, make_expected(S05b, std::unordered_set<std::string>{}, aedis::error::expects_resp3_set)); \
test(ex, make_expected(s05c, array_type2{}, aedis::error::expects_resp3_aggregate));\
test(ex, make_expected(s05c, node_type{resp3::type::number, 1UL, 0UL, {"3"}})); \
test(ex, make_expected(S06a, array_type{}, aedis::error::resp3_null));\
test(ex, make_expected(S06a, int{0}, aedis::error::resp3_null)); \
test(ex, make_expected(S06a, map_type{}, aedis::error::resp3_null));\
test(ex, make_expected(S06a, op_type_01{}));\
test(ex, make_expected(S06a, op_type_02{}));\
test(ex, make_expected(S06a, op_type_03{}));\
test(ex, make_expected(S06a, op_type_04{}));\
test(ex, make_expected(S06a, op_type_05{}));\
test(ex, make_expected(S06a, op_type_06{}));\
test(ex, make_expected(S06a, op_type_07{}));\
test(ex, make_expected(S06a, op_type_08{}));\
test(ex, make_expected(S06a, op_type_09{}));\
test(ex, make_expected(S06a, std::list<int>{}, aedis::error::resp3_null));\
test(ex, make_expected(S06a, std::vector<int>{}, aedis::error::resp3_null));\
test(ex, make_expected(S07a, push_e1a)); \
test(ex, make_expected(S07b, push_e1b)); \
test(ex, make_expected(S04b, map_type{}, aedis::error::expects_resp3_map));\
test(ex, make_expected(S03b, map_e1f));\
test(ex, make_expected(S03b, map_e1g));\
test(ex, make_expected(S03b, map_e1k));\
test(ex, make_expected(S03b, map_e1l));\
test(ex, make_expected(S03b, map_expected_1a));\
test(ex, make_expected(S03b, map_expected_1b));\
test(ex, make_expected(S03b, map_expected_1c));\
test(ex, make_expected(S03b, map_expected_1d));\
test(ex, make_expected(S03b, map_expected_1e));\
test(ex, make_expected(S04c, std::tuple<op_map_type>{map_expected_1d}));\
test(ex, make_expected(S08a, attr_e1a)); \
test(ex, make_expected(S08b, attr_e1b)); \
test(ex, make_expected(S04e, array_e1a));\
test(ex, make_expected(S04e, array_e1b));\
test(ex, make_expected(S04e, array_e1c));\
test(ex, make_expected(S04e, array_e1f));\
test(ex, make_expected(S04e, array_e1g));\
test(ex, make_expected(S04e, array_e1h));\
test(ex, make_expected(S04e, array_type2{}, aedis::error::incompatible_size));\
test(ex, make_expected(S04e, tuple_int_2{}, aedis::error::incompatible_size));\
test(ex, make_expected(S04f, array_type2{}, aedis::error::nested_aggregate_not_supported));\
test(ex, make_expected(S04g, vec_node_type{}, aedis::error::exceeeds_max_nested_depth));\
test(ex, make_expected(S04h, array_e1d));\
test(ex, make_expected(S04h, array_e1e));\
test(ex, make_expected(S04i, set_type{}, aedis::error::expects_resp3_set)); \
test(ex, make_expected(S09a, set_e1c)); \
test(ex, make_expected(S09a, set_e1d)); \
test(ex, make_expected(S09a, set_e1f)); \
test(ex, make_expected(S09a, set_e1g)); \
test(ex, make_expected(S09a, set_expected1a)); \
test(ex, make_expected(S09a, set_expected_1e)); \
test(ex, make_expected(S09a, set_type{"apple", "one", "orange", "three", "two"})); \
test(ex, make_expected(S04d, std::tuple<uset_type>{set_e1c})); \
test(ex, make_expected(S09b, std::vector<node_type>{ {resp3::type::set, 0UL, 0UL, {}} })); \
test(ex, make_expected(S10a, aedis::ignore{}, aedis::error::resp3_simple_error)); \
test(ex, make_expected(S10a, node_type{resp3::type::simple_error, 1UL, 0UL, {"Error"}}, aedis::error::resp3_simple_error)); \
test(ex, make_expected(S10b, node_type{resp3::type::simple_error, 1UL, 0UL, {""}}, aedis::error::resp3_simple_error)); \
test(ex, make_expected(S03c, map_type{}));\
test(ex, make_expected(S11a, node_type{resp3::type::doublean, 1UL, 0UL, {"1.23"}}));\
test(ex, make_expected(S11b, node_type{resp3::type::doublean, 1UL, 0UL, {"inf"}}));\
test(ex, make_expected(S11c, node_type{resp3::type::doublean, 1UL, 0UL, {"-inf"}}));\
test(ex, make_expected(S11d, double{1.23}));\
test(ex, make_expected(S11e, double{0}, aedis::error::not_a_double));\
test(ex, make_expected(S12a, node_type{resp3::type::blob_error, 1UL, 0UL, {"SYNTAX invalid syntax"}}, aedis::error::resp3_blob_error));\
test(ex, make_expected(S12b, node_type{resp3::type::blob_error, 1UL, 0UL, {}}, aedis::error::resp3_blob_error));\
test(ex, make_expected(S12c, aedis::ignore{}, aedis::error::resp3_blob_error));\
test(ex, make_expected(S13a, node_type{resp3::type::verbatim_string, 1UL, 0UL, {"txt:Some string"}}));\
test(ex, make_expected(S13b, node_type{resp3::type::verbatim_string, 1UL, 0UL, {}}));\
test(ex, make_expected(S14a, node_type{resp3::type::big_number, 1UL, 0UL, {"3492890328409238509324850943850943825024385"}}));\
test(ex, make_expected(S14b, int{}, aedis::error::empty_field));\
test(ex, make_expected(S15a, std::optional<std::string>{"OK"}));\
test(ex, make_expected(S15a, std::string{"OK"}));\
test(ex, make_expected(S15b, std::optional<std::string>{""}));\
test(ex, make_expected(S15b, std::string{""}));\
test(ex, make_expected(S16a, int{}, aedis::error::invalid_data_type));\
test(ex, make_expected(S05d, int{11}, aedis::error::not_a_number));\
test(ex, make_expected(S03d, map_type{}, aedis::error::not_a_number));\
test(ex, make_expected(S02d, std::string{}, aedis::error::not_a_number));\
test(ex, make_expected(S17a, std::string{}, aedis::error::not_a_number));\
test(ex, make_expected(S05e, int{}, aedis::error::empty_field));\
test(ex, make_expected(S01d, std::optional<bool>{}, aedis::error::empty_field));\
test(ex, make_expected(S11f, std::string{}, aedis::error::empty_field));\
test(ex, make_expected(S17b, node_type{resp3::type::blob_string, 1UL, 0UL, {"hh"}}));\
test(ex, make_expected(S18c, node_type{resp3::type::blob_string, 1UL, 0UL, {"hhaa\aaaa\raaaaa\r\naaaaaaaaaa"}}));\
test(ex, make_expected(S18d, node_type{resp3::type::blob_string, 1UL, 0UL, {}}));\
test(ex, make_expected(make_blob_string(blob), node_type{resp3::type::blob_string, 1UL, 0UL, {blob}}));\
BOOST_AUTO_TEST_CASE(parser)
{
net::io_context ioc;
auto ex = ioc.get_executor();
#define TEST test_sync
NUMBER_TEST_CONDITIONS(TEST)
#undef TEST
#define TEST test_async
NUMBER_TEST_CONDITIONS(TEST)
#undef TEST
ioc.run();
}
BOOST_AUTO_TEST_CASE(ignore_adapter_simple_error)
{
net::io_context ioc;
std::string rbuffer;
boost::system::error_code ec;
test_stream ts {ioc};
ts.append(S10a);
resp3::read(ts, net::dynamic_buffer(rbuffer), adapt2(), ec);
BOOST_CHECK_EQUAL(ec, aedis::error::resp3_simple_error);
BOOST_TEST(!rbuffer.empty());
}
BOOST_AUTO_TEST_CASE(ignore_adapter_blob_error)
{
net::io_context ioc;
std::string rbuffer;
boost::system::error_code ec;
test_stream ts {ioc};
ts.append(S12a);
resp3::read(ts, net::dynamic_buffer(rbuffer), adapt2(), ec);
BOOST_CHECK_EQUAL(ec, aedis::error::resp3_blob_error);
BOOST_TEST(!rbuffer.empty());
}
BOOST_AUTO_TEST_CASE(ignore_adapter_no_error)
{
net::io_context ioc;
std::string rbuffer;
boost::system::error_code ec;
test_stream ts {ioc};
ts.append(S05b);
resp3::read(ts, net::dynamic_buffer(rbuffer), adapt2(), ec);
BOOST_TEST(!ec);
BOOST_TEST(rbuffer.empty());
}
//-----------------------------------------------------------------------------------
void check_error(char const* name, aedis::error ev)
{
auto const ec = aedis::make_error_code(ev);
auto const& cat = ec.category();
BOOST_TEST(std::string(ec.category().name()) == name);
BOOST_TEST(!ec.message().empty());
BOOST_TEST(cat.equivalent(
static_cast<std::underlying_type<aedis::error>::type>(ev),
ec.category().default_error_condition(
static_cast<std::underlying_type<aedis::error>::type>(ev))));
BOOST_TEST(cat.equivalent(ec,
static_cast<std::underlying_type<aedis::error>::type>(ev)));
}
BOOST_AUTO_TEST_CASE(error)
{
check_error("aedis", aedis::error::invalid_data_type);
check_error("aedis", aedis::error::not_a_number);
check_error("aedis", aedis::error::exceeeds_max_nested_depth);
check_error("aedis", aedis::error::unexpected_bool_value);
check_error("aedis", aedis::error::empty_field);
check_error("aedis", aedis::error::expects_resp3_simple_type);
check_error("aedis", aedis::error::expects_resp3_aggregate);
check_error("aedis", aedis::error::expects_resp3_map);
check_error("aedis", aedis::error::expects_resp3_set);
check_error("aedis", aedis::error::nested_aggregate_not_supported);
check_error("aedis", aedis::error::resp3_simple_error);
check_error("aedis", aedis::error::resp3_blob_error);
check_error("aedis", aedis::error::incompatible_size);
check_error("aedis", aedis::error::not_a_double);
check_error("aedis", aedis::error::resp3_null);
check_error("aedis", aedis::error::not_connected);
}
std::string get_type_as_str(aedis::resp3::type t)
{
std::ostringstream ss;
ss << t;
return ss.str();
}
BOOST_AUTO_TEST_CASE(type_string)
{
BOOST_TEST(!get_type_as_str(aedis::resp3::type::array).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::push).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::set).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::map).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::attribute).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::simple_string).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::simple_error).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::number).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::doublean).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::boolean).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::big_number).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::null).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::blob_error).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::verbatim_string).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::blob_string).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::streamed_string_part).empty());
BOOST_TEST(!get_type_as_str(aedis::resp3::type::invalid).empty());
}
BOOST_AUTO_TEST_CASE(type_convert)
{
using aedis::resp3::to_code;
using aedis::resp3::to_type;
using aedis::resp3::type;
#define CHECK_CASE(A) BOOST_CHECK_EQUAL(to_type(to_code(type::A)), type::A);
CHECK_CASE(array);
CHECK_CASE(push);
CHECK_CASE(set);
CHECK_CASE(map);
CHECK_CASE(attribute);
CHECK_CASE(simple_string);
CHECK_CASE(simple_error);
CHECK_CASE(number);
CHECK_CASE(doublean);
CHECK_CASE(boolean);
CHECK_CASE(big_number);
CHECK_CASE(null);
CHECK_CASE(blob_error);
CHECK_CASE(verbatim_string);
CHECK_CASE(blob_string);
CHECK_CASE(streamed_string_part);
#undef CHECK_CASE
}
BOOST_AUTO_TEST_CASE(adapter)
{
using aedis::adapt;
using resp3::type;
boost::system::error_code ec;
std::string a;
int b;
auto resp = std::tie(a, b, std::ignore);
auto f = adapt(resp);
f(0, resp3::node<std::string_view>{type::simple_string, 1, 0, "Hello"}, ec);
f(1, resp3::node<std::string_view>{type::number, 1, 0, "42"}, ec);
BOOST_CHECK_EQUAL(a, "Hello");
BOOST_TEST(!ec);
BOOST_CHECK_EQUAL(b, 42);
BOOST_TEST(!ec);
}

View File

@@ -0,0 +1,128 @@
/* 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/system/errc.hpp>
#define BOOST_TEST_MODULE check-health
#include <boost/test/included/unit_test.hpp>
#include <iostream>
#include <thread>
#include "common.hpp"
namespace net = boost::asio;
namespace redis = boost::redis;
using error_code = boost::system::error_code;
using connection = boost::redis::connection;
using boost::redis::request;
using boost::redis::ignore;
using boost::redis::operation;
using boost::redis::generic_response;
using boost::redis::logger;
using redis::config;
// TODO: Test cancel(health_check)
std::chrono::seconds const interval{1};
struct push_callback {
connection* conn1;
connection* conn2;
generic_response* resp2;
request* req1;
int i = 0;
boost::asio::coroutine coro{};
void operator()(error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro) for (;;)
{
resp2->value().clear();
BOOST_ASIO_CORO_YIELD
conn2->async_receive(*resp2, *this);
if (ec) {
std::clog << "Exiting." << std::endl;
return;
}
BOOST_TEST(resp2->has_value());
BOOST_TEST(!resp2->value().empty());
std::clog << "Event> " << resp2->value().front().value << std::endl;
++i;
if (i == 5) {
std::clog << "Pausing the server" << std::endl;
// Pause the redis server to test if the health-check exits.
BOOST_ASIO_CORO_YIELD
conn1->async_exec(*req1, ignore, *this);
std::clog << "After pausing> " << ec.message() << std::endl;
// Don't know in CI we are getting: Got RESP3 simple-error.
//BOOST_TEST(!ec);
conn2->cancel(operation::run);
conn2->cancel(operation::receive);
conn2->cancel(operation::reconnection);
return;
}
}
};
};
BOOST_AUTO_TEST_CASE(check_health)
{
net::io_context ioc;
connection conn1{ioc};
request req1;
req1.push("CLIENT", "PAUSE", "10000", "ALL");
config cfg1;
cfg1.health_check_id = "conn1";
cfg1.reconnect_wait_interval = std::chrono::seconds::zero();
error_code res1;
conn1.async_run(cfg1, {}, [&](auto ec) {
std::cout << "async_run 1 completed: " << ec.message() << std::endl;
res1 = ec;
});
//--------------------------------
// It looks like client pause does not work for clients that are
// sending MONITOR. I will therefore open a second connection.
connection conn2{ioc};
config cfg2;
cfg2.health_check_id = "conn2";
error_code res2;
conn2.async_run(cfg2, {}, [&](auto ec){
std::cout << "async_run 2 completed: " << ec.message() << std::endl;
res2 = ec;
});
request req2;
req2.push("MONITOR");
generic_response resp2;
conn2.async_exec(req2, ignore, [](auto ec, auto) {
std::cout << "async_exec: " << std::endl;
BOOST_TEST(!ec);
});
//--------------------------------
push_callback{&conn1, &conn2, &resp2, &req1}(); // Starts reading pushes.
ioc.run();
BOOST_TEST(!!res1);
BOOST_TEST(!!res2);
// Waits before exiting otherwise it might cause subsequent tests
// to fail.
std::this_thread::sleep_for(std::chrono::seconds{10});
}

View File

@@ -0,0 +1,125 @@
/* 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/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE echo-stress
#include <boost/test/included/unit_test.hpp>
#include <iostream>
#include "common.hpp"
#ifdef BOOST_ASIO_HAS_CO_AWAIT
namespace net = boost::asio;
using error_code = boost::system::error_code;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::logger;
using boost::redis::config;
using boost::redis::connection;
auto push_consumer(std::shared_ptr<connection> conn, int expected) -> net::awaitable<void>
{
int c = 0;
for (;;) {
co_await conn->async_receive(ignore, net::use_awaitable);
if (++c == expected)
break;
}
conn->cancel();
}
auto
echo_session(
std::shared_ptr<connection> conn,
std::shared_ptr<request> pubs,
std::string id,
int n) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
request req;
response<ignore_t, std::string, ignore_t> resp;
for (auto i = 0; i < n; ++i) {
auto const msg = id + "/" + std::to_string(i);
//std::cout << msg << std::endl;
req.push("HELLO", 3); // Just to mess around.
req.push("PING", msg);
req.push("PING", "lsls"); // TODO: Change to HELLO after fixing issue 105.
boost::system::error_code ec;
co_await conn->async_exec(req, resp, redir(ec));
BOOST_REQUIRE_EQUAL(ec, boost::system::error_code{});
BOOST_REQUIRE_EQUAL(msg, std::get<1>(resp).value());
req.clear();
std::get<1>(resp).value().clear();
co_await conn->async_exec(*pubs, ignore, net::deferred);
}
}
auto async_echo_stress() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
config cfg;
cfg.health_check_interval = std::chrono::seconds::zero();
run(conn, cfg,
boost::asio::error::operation_aborted,
boost::redis::operation::receive,
boost::redis::logger::level::crit);
request req;
req.push("SUBSCRIBE", "channel");
co_await conn->async_exec(req, ignore, net::deferred);
// Number of coroutines that will send pings sharing the same
// connection to redis.
int const sessions = 500;
// The number of pings that will be sent by each session.
int const msgs = 1000;
// The number of publishes that will be sent by each session with
// each message.
int const n_pubs = 10;
// This is the total number of pushes we will receive.
int total_pushes = sessions * msgs * n_pubs + 1;
auto pubs = std::make_shared<request>();
for (int i = 0; i < n_pubs; ++i)
pubs->push("PUBLISH", "channel", "payload");
// Op that will consume the pushes counting down until all expected
// pushes have been received.
net::co_spawn(ex, push_consumer(conn, total_pushes), net::detached);
for (int i = 0; i < sessions; ++i)
net::co_spawn(ex, echo_session(conn, pubs, std::to_string(i), msgs), net::detached);
}
BOOST_AUTO_TEST_CASE(echo_stress)
{
net::io_context ioc;
net::co_spawn(ioc, async_echo_stress(), net::detached);
ioc.run();
}
#else
BOOST_AUTO_TEST_CASE(dummy)
{
BOOST_TEST(true);
}
#endif

156
tests/test_conn_exec.cpp Normal file
View File

@@ -0,0 +1,156 @@
/* 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/system/errc.hpp>
#define BOOST_TEST_MODULE conn-exec
#include <boost/test/included/unit_test.hpp>
#include <iostream>
#include "common.hpp"
// TODO: Test whether HELLO won't be inserted passt commands that have
// been already writen.
// TODO: Test async_exec with empty request e.g. hgetall with an empty
// container.
namespace net = boost::asio;
using boost::redis::connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::operation;
using boost::redis::config;
// Sends three requests where one of them has a hello with a priority
// set, which means it should be executed first.
BOOST_AUTO_TEST_CASE(hello_priority)
{
request req1;
req1.push("PING", "req1");
request req2;
req2.get_config().hello_with_priority = false;
req2.push("HELLO", 3);
req2.push("PING", "req2");
request req3;
req3.get_config().hello_with_priority = true;
req3.push("HELLO", 3);
req3.push("PING", "req3");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
bool seen1 = false;
bool seen2 = false;
bool seen3 = false;
conn->async_exec(req1, ignore, [&](auto ec, auto){
// Second callback to the called.
std::cout << "req1" << std::endl;
BOOST_CHECK_EQUAL(ec, boost::system::error_code{});
BOOST_TEST(!seen2);
BOOST_TEST(seen3);
seen1 = true;
});
conn->async_exec(req2, ignore, [&](auto ec, auto){
// Last callback to the called.
std::cout << "req2" << std::endl;
BOOST_CHECK_EQUAL(ec, boost::system::error_code{});
BOOST_TEST(seen1);
BOOST_TEST(seen3);
seen2 = true;
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
conn->async_exec(req3, ignore, [&](auto ec, auto){
// Callback that will be called first.
std::cout << "req3" << std::endl;
BOOST_CHECK_EQUAL(ec, boost::system::error_code{});
BOOST_TEST(!seen1);
BOOST_TEST(!seen2);
seen3 = true;
});
run(conn);
ioc.run();
}
// Tries to receive a string in an int and gets an error.
BOOST_AUTO_TEST_CASE(wrong_response_data_type)
{
request req;
req.push("PING");
// Wrong data type.
response<int> resp;
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
conn->async_exec(req, resp, [conn](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::redis::error::not_a_number);
conn->cancel(operation::reconnection);
});
run(conn);
ioc.run();
}
BOOST_AUTO_TEST_CASE(cancel_request_if_not_connected)
{
request req;
req.get_config().cancel_if_not_connected = true;
req.push("PING");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
conn->async_exec(req, ignore, [conn](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::redis::error::not_connected);
conn->cancel();
});
ioc.run();
}
BOOST_AUTO_TEST_CASE(correct_database)
{
config cfg;
cfg.database_index = 2;
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req;
req.push("CLIENT", "LIST");
generic_response resp;
conn->async_exec(req, resp, [&](auto ec, auto n){
BOOST_TEST(!ec);
std::clog << "async_exec has completed: " << n << std::endl;
conn->cancel();
});
conn->async_run(cfg, {}, [](auto){
std::clog << "async_run has exited." << std::endl;
});
ioc.run();
assert(!resp.value().empty());
auto const& value = resp.value().front().value;
auto const pos = value.find("db=");
auto const index_str = value.substr(pos + 3, 1);
auto const index = std::stoi(index_str);
BOOST_CHECK_EQUAL(cfg.database_index.value(), index);
}

View File

@@ -0,0 +1,129 @@
/* 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/system/errc.hpp>
#define BOOST_TEST_MODULE conn-exec-cancel
#include <boost/test/included/unit_test.hpp>
#include <boost/asio/detached.hpp>
#include "common.hpp"
#include <iostream>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/asio/experimental/awaitable_operators.hpp>
// NOTE1: I have observed that if hello and
// blpop are sent toguether, Redis will send the response of hello
// right away, not waiting for blpop.
namespace net = boost::asio;
using error_code = boost::system::error_code;
using namespace net::experimental::awaitable_operators;
using boost::redis::operation;
using boost::redis::error;
using boost::redis::request;
using boost::redis::response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::logger;
using boost::redis::connection;
using namespace std::chrono_literals;
auto implicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
config cfg;
cfg.health_check_interval = std::chrono::seconds{0};
run(conn, cfg);
// See NOTE1.
request req0;
req0.push("PING");
co_await conn->async_exec(req0, ignore, net::use_awaitable);
// Will be cancelled after it has been written but before the
// response arrives.
request req1;
req1.push("BLPOP", "any", 3);
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
// Achieves implicit cancellation when the timer fires.
boost::system::error_code ec1, ec2;
co_await (
conn->async_exec(req1, ignore, redir(ec1)) ||
st.async_wait(redir(ec2))
);
conn->cancel();
// I have observed this produces terminal cancellation so it can't
// be ignored, an error is expected.
BOOST_CHECK_EQUAL(ec1, net::error::operation_aborted);
BOOST_TEST(!ec2);
}
BOOST_AUTO_TEST_CASE(test_ignore_implicit_cancel_of_req_written)
{
net::io_context ioc;
net::co_spawn(ioc, implicit_cancel_of_req_written(), net::detached);
ioc.run();
}
BOOST_AUTO_TEST_CASE(test_cancel_of_req_written_on_run_canceled)
{
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req0;
req0.push("PING");
// Sends a request that will be blocked forever, so we can test
// canceling it while waiting for a response.
request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.get_config().cancel_if_unresponded = true;
req1.push("BLPOP", "any", 0);
auto c1 = [&](auto ec, auto)
{
BOOST_CHECK_EQUAL(ec, net::error::operation_aborted);
};
auto c0 = [&](auto ec, auto)
{
BOOST_TEST(!ec);
conn->async_exec(req1, ignore, c1);
};
conn->async_exec(req0, ignore, c0);
config cfg;
cfg.health_check_interval = std::chrono::seconds{5};
run(conn);
net::steady_timer st{ioc};
st.expires_after(std::chrono::seconds{1});
st.async_wait([&](auto ec){
BOOST_TEST(!ec);
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
ioc.run();
}
#else
BOOST_AUTO_TEST_CASE(dummy)
{
BOOST_TEST(true);
}
#endif

View 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/system/errc.hpp>
#define BOOST_TEST_MODULE conn-exec-cancel
#include <boost/test/included/unit_test.hpp>
#include <boost/asio/detached.hpp>
#include "common.hpp"
#include <iostream>
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/asio/experimental/awaitable_operators.hpp>
// NOTE1: Sends hello separately. I have observed that if hello and
// blpop are sent toguether, Redis will send the response of hello
// right away, not waiting for blpop. That is why we have to send it
// separately.
namespace net = boost::asio;
using error_code = boost::system::error_code;
using namespace net::experimental::awaitable_operators;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::logger;
using boost::redis::connection;
using namespace std::chrono_literals;
auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
generic_response gresp;
auto conn = std::make_shared<connection>(ex);
run(conn);
net::steady_timer st{ex};
st.expires_after(std::chrono::seconds{1});
// See NOTE1.
request req0;
req0.push("PING", "async_ignore_explicit_cancel_of_req_written");
co_await conn->async_exec(req0, gresp, net::use_awaitable);
request req1;
req1.push("BLPOP", "any", 3);
bool seen = false;
conn->async_exec(req1, gresp, [&](auto ec, auto) mutable{
// No error should occur since the cancelation should be
// ignored.
std::cout << "async_exec (1): " << ec.message() << std::endl;
BOOST_TEST(!ec);
seen = true;
});
// Will complete while BLPOP is pending.
boost::system::error_code ec1;
co_await st.async_wait(net::redirect_error(net::use_awaitable, ec1));
conn->cancel(operation::exec);
BOOST_TEST(!ec1);
request req3;
req3.push("PING");
// Test whether the connection remains usable after a call to
// cancel(exec).
co_await conn->async_exec(req3, gresp, net::redirect_error(net::use_awaitable, ec1));
conn->cancel();
BOOST_TEST(!ec1);
BOOST_TEST(seen);
}
BOOST_AUTO_TEST_CASE(test_ignore_explicit_cancel_of_req_written)
{
net::io_context ioc;
net::co_spawn(ioc, async_ignore_explicit_cancel_of_req_written(), net::detached);
ioc.run();
}
#else
BOOST_AUTO_TEST_CASE(dummy)
{
BOOST_TEST(true);
}
#endif

View File

@@ -0,0 +1,263 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE conn-exec-error
#include <boost/test/included/unit_test.hpp>
#include "common.hpp"
#include <iostream>
namespace net = boost::asio;
namespace redis = boost::redis;
namespace resp3 = redis::resp3;
using error_code = boost::system::error_code;
using connection = boost::redis::connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::error;
using boost::redis::logger;
using boost::redis::operation;
using redis::config;
using namespace std::chrono_literals;
BOOST_AUTO_TEST_CASE(no_ignore_error)
{
request req;
// HELLO expects a number, by feeding a string we should get a simple error.
req.push("HELLO", "not-a-number");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
conn->async_exec(req, ignore, [&](auto ec, auto){
BOOST_CHECK_EQUAL(ec, error::resp3_simple_error);
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
run(conn);
ioc.run();
}
BOOST_AUTO_TEST_CASE(has_diagnostic)
{
request req;
// HELLO expects a number, by feeding a string we should get a simple error.
req.push("HELLO", "not-a-number");
// The second command should be also executed. Notice PING does not
// require resp3.
req.push("PING", "Barra do Una");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
response<std::string, std::string> resp;
conn->async_exec(req, resp, [&](auto ec, auto){
BOOST_TEST(!ec);
// HELLO
BOOST_TEST(std::get<0>(resp).has_error());
BOOST_CHECK_EQUAL(std::get<0>(resp).error().data_type, resp3::type::simple_error);
auto const diag = std::get<0>(resp).error().diagnostic;
BOOST_TEST(!std::empty(diag));
std::cout << "has_diagnostic: " << diag << std::endl;
// PING
BOOST_TEST(std::get<1>(resp).has_value());
BOOST_CHECK_EQUAL(std::get<1>(resp).value(), "Barra do Una");
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
run(conn);
ioc.run();
}
BOOST_AUTO_TEST_CASE(resp3_error_in_cmd_pipeline)
{
request req1;
req1.push("HELLO", "3");
req1.push("PING", "req1-msg1");
req1.push("PING", "req1-msg2", "extra arg"); // Error.
req1.push("PING", "req1-msg3"); // Should run ok.
response<ignore_t, std::string, std::string, std::string> resp1;
request req2;
req2.push("PING", "req2-msg1");
response<std::string> resp2;
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
auto c2 = [&](auto ec, auto)
{
BOOST_TEST(!ec);
BOOST_TEST(std::get<0>(resp2).has_value());
BOOST_CHECK_EQUAL(std::get<0>(resp2).value(), "req2-msg1");
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
};
auto c1 = [&](auto ec, auto)
{
BOOST_TEST(!ec);
BOOST_TEST(std::get<2>(resp1).has_error());
BOOST_CHECK_EQUAL(std::get<2>(resp1).error().data_type, resp3::type::simple_error);
auto const diag = std::get<2>(resp1).error().diagnostic;
BOOST_TEST(!std::empty(diag));
std::cout << "resp3_error_in_cmd_pipeline: " << diag << std::endl;
BOOST_TEST(std::get<3>(resp1).has_value());
BOOST_CHECK_EQUAL(std::get<3>(resp1).value(), "req1-msg3");
conn->async_exec(req2, resp2, c2);
};
conn->async_exec(req1, resp1, c1);
run(conn);
ioc.run();
}
BOOST_AUTO_TEST_CASE(error_in_transaction)
{
request req;
req.push("HELLO", 3);
req.push("MULTI");
req.push("PING");
req.push("PING", "msg2", "error"); // Error.
req.push("PING");
req.push("EXEC");
req.push("PING");
response<
ignore_t, // hello
ignore_t, // multi
ignore_t, // ping
ignore_t, // ping
ignore_t, // ping
response<std::string, std::string, std::string>, // exec
std::string // ping
> resp;
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
conn->async_exec(req, resp, [&](auto ec, auto){
BOOST_TEST(!ec);
BOOST_TEST(std::get<0>(resp).has_value());
BOOST_TEST(std::get<1>(resp).has_value());
BOOST_TEST(std::get<2>(resp).has_value());
BOOST_TEST(std::get<3>(resp).has_value());
BOOST_TEST(std::get<4>(resp).has_value());
BOOST_TEST(std::get<5>(resp).has_value());
// Test errors in the pipeline commands.
BOOST_TEST(std::get<0>(std::get<5>(resp).value()).has_value());
BOOST_CHECK_EQUAL(std::get<0>(std::get<5>(resp).value()).value(), "PONG");
// The ping in the transaction that should be an error.
BOOST_TEST(std::get<1>(std::get<5>(resp).value()).has_error());
BOOST_CHECK_EQUAL(std::get<1>(std::get<5>(resp).value()).error().data_type, resp3::type::simple_error);
auto const diag = std::get<1>(std::get<5>(resp).value()).error().diagnostic;
BOOST_TEST(!std::empty(diag));
// The ping thereafter in the transaction should not be an error.
BOOST_TEST(std::get<2>(std::get<5>(resp).value()).has_value());
//BOOST_CHECK_EQUAL(std::get<2>(std::get<4>(resp).value()).value(), "PONG");
// The command right after the pipeline should be successful.
BOOST_TEST(std::get<6>(resp).has_value());
BOOST_CHECK_EQUAL(std::get<6>(resp).value(), "PONG");
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
});
run(conn);
ioc.run();
}
// This test is important because a SUBSCRIBE command has no response
// on success, but does on error. for example when using a wrong
// syntax, the server will send a simple error response the client is
// not expecting.
//
// Sending the subscribe after the ping command below is just a
// convenience to avoid have it merged in a pipeline making things
// even more complex. For example, without a ping, we might get the
// sequence HELLO + SUBSCRIBE + PING where the hello and ping are
// automatically sent by the implementation. In this case, if the
// subscribe synthax is wrong, redis will send a response, which does
// not exist on success. That response will be interprested as the
// response to the PING command that comes thereafter and won't be
// forwarded to the receive_op, resulting in a difficult to handle
// error.
BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
{
request req1;
req1.push("PING");
request req2;
req2.push("SUBSCRIBE"); // Wrong command synthax.
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
auto c2 = [&](auto ec, auto)
{
std::cout << "async_exec: subscribe" << std::endl;
BOOST_TEST(!ec);
};
auto c1 = [&](auto ec, auto)
{
std::cout << "async_exec: hello" << std::endl;
BOOST_TEST(!ec);
conn->async_exec(req2, ignore, c2);
};
conn->async_exec(req1, ignore, c1);
generic_response gresp;
auto c3 = [&](auto ec, auto)
{
std::cout << "async_receive" << std::endl;
BOOST_TEST(!ec);
BOOST_TEST(gresp.has_error());
BOOST_CHECK_EQUAL(gresp.error().data_type, resp3::type::simple_error);
BOOST_TEST(!std::empty(gresp.error().diagnostic));
std::cout << gresp.error().diagnostic << std::endl;
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
};
conn->async_receive(gresp, c3);
run(conn);
ioc.run();
}

View File

@@ -4,44 +4,43 @@
* accompanying file LICENSE.txt)
*/
#include <iostream>
#include <boost/asio.hpp>
#include <boost/redis/connection.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/system/errc.hpp>
#define BOOST_TEST_MODULE low level
#define BOOST_TEST_MODULE conn-exec-retry
#include <boost/test/included/unit_test.hpp>
#include <aedis.hpp>
#include <aedis/src.hpp>
#include <iostream>
#include "common.hpp"
namespace net = boost::asio;
namespace resp3 = aedis::resp3;
using error_code = boost::system::error_code;
using connection = aedis::connection;
using aedis::adapt;
using connection = boost::redis::connection;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore;
using boost::redis::logger;
using boost::redis::config;
using namespace std::chrono_literals;
BOOST_AUTO_TEST_CASE(request_retry_false)
{
resp3::request req0;
req0.get_config().coalesce = false;
request req0;
req0.get_config().cancel_on_connection_lost = true;
req0.push("HELLO", 3);
resp3::request req1;
req1.get_config().coalesce = true;
request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.push("BLPOP", "any", 0);
resp3::request req2;
req2.get_config().coalesce = true;
request req2;
req2.get_config().cancel_on_connection_lost = false;
req2.get_config().cancel_if_unresponded = true;
req2.push("PING");
net::io_context ioc;
connection conn{ioc};
auto conn = std::make_shared<connection>(ioc);
net::steady_timer st{ioc};
st.expires_after(std::chrono::seconds{1});
@@ -49,99 +48,100 @@ BOOST_AUTO_TEST_CASE(request_retry_false)
// Cancels the request before receiving the response. This
// should cause the third request to complete with error
// although it has cancel_on_connection_lost = false. The reason
// being is has already been written so
// being it has already been written so
// cancel_on_connection_lost does not apply.
conn.cancel(aedis::operation::run);
conn->cancel(operation::run);
conn->cancel(operation::reconnection);
std::cout << "async_wait" << std::endl;
});
auto const endpoints = resolve();
net::connect(conn.next_layer(), endpoints);
auto c2 = [&](auto ec, auto){
std::cout << "c2" << std::endl;
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
};
conn.async_exec(req0, adapt(), [](auto ec, auto){
auto c1 = [&](auto ec, auto){
std::cout << "c1" << std::endl;
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
};
auto c0 = [&](auto ec, auto){
std::cout << "c0" << std::endl;
BOOST_TEST(!ec);
});
conn->async_exec(req1, ignore, c1);
conn->async_exec(req2, ignore, c2);
};
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn->async_exec(req0, ignore, c0);
conn.async_exec(req2, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_run([](auto ec){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
config cfg;
cfg.health_check_interval = 5s;
run(conn);
ioc.run();
}
BOOST_AUTO_TEST_CASE(request_retry_true)
{
resp3::request req0;
req0.get_config().coalesce = false;
request req0;
req0.get_config().cancel_on_connection_lost = true;
req0.push("HELLO", 3);
resp3::request req1;
req1.get_config().coalesce = true;
request req1;
req1.get_config().cancel_on_connection_lost = true;
req1.push("BLPOP", "any", 0);
resp3::request req2;
req2.get_config().coalesce = true;
request req2;
req2.get_config().cancel_on_connection_lost = false;
req2.get_config().cancel_if_unresponded = false;
req2.push("PING");
resp3::request req3;
req3.get_config().coalesce = true;
request req3;
req3.get_config().cancel_on_connection_lost = true;
req3.get_config().cancel_if_unresponded = true;
req3.push("QUIT");
net::io_context ioc;
connection conn{ioc};
auto conn = std::make_shared<connection>(ioc);
net::steady_timer st{ioc};
st.expires_after(std::chrono::seconds{1});
st.async_wait([&](auto){
// Cancels the request before receiving the response. This
// should cause the thrid request to not complete with error
// since it has cancel_if_unresponded = true and cancellation commes
// after it was written.
conn.cancel(aedis::operation::run);
// since it has cancel_if_unresponded = true and cancellation
// comes after it was written.
conn->cancel(operation::run);
});
auto const endpoints = resolve();
net::connect(conn.next_layer(), endpoints);
conn.async_exec(req0, adapt(), [](auto ec, auto){
auto c3 = [&](auto ec, auto){
std::cout << "c3: " << ec.message() << std::endl;
BOOST_TEST(!ec);
});
conn->cancel();
};
conn.async_exec(req1, adapt(), [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
});
conn.async_exec(req2, adapt(), [&](auto ec, auto){
auto c2 = [&](auto ec, auto){
BOOST_TEST(!ec);
conn.async_exec(req3, adapt(), [&](auto ec, auto){
BOOST_TEST(!ec);
});
});
conn->async_exec(req3, ignore, c3);
};
conn.async_run([&](auto ec){
// The first cacellation.
auto c1 = [](auto ec, auto){
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
conn.reset_stream();
};
// Reconnects and runs again to test req3.
net::connect(conn.next_layer(), endpoints);
conn.async_run([&](auto ec){
std::cout << ec.message() << std::endl;
BOOST_TEST(!ec);
});
auto c0 = [&](auto ec, auto){
BOOST_TEST(!ec);
conn->async_exec(req1, ignore, c1);
conn->async_exec(req2, ignore, c2);
};
conn->async_exec(req0, ignore, c0);
config cfg;
cfg.health_check_interval = 5s;
conn->async_run(cfg, {}, [&](auto ec){
std::cout << ec.message() << std::endl;
BOOST_TEST(!!ec);
});
ioc.run();

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