diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc1e2c08..3fe46a04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -115,6 +115,8 @@ jobs: 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 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 00bb498e..8b944b26 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,7 +3,7 @@ name: Coverage on: push: branches: - - master + - develop jobs: posix: defaults: diff --git a/CMakeLists.txt b/CMakeLists.txt index 113f4902..c9022131 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,16 @@ 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( boost_redis VERSION 1.4.1 @@ -8,6 +19,12 @@ project( LANGUAGES CXX ) +option(BOOST_REDIS_INSTALL "Generate install targets." ${BOOST_REDIS_MAIN_PROJECT}) +option(BOOST_REDIS_TESTS "Build tests." ${BOOST_REDIS_MAIN_PROJECT}) +option(BOOST_REDIS_EXAMPLES "Build examples." ${BOOST_REDIS_MAIN_PROJECT}) +option(BOOST_REDIS_BENCHMARKS "Build benchmarks." ${BOOST_REDIS_MAIN_PROJECT}) +option(BOOST_REDIS_DOC "Generate documentations." ${BOOST_REDIS_MAIN_PROJECT}) + add_library(boost_redis INTERFACE) add_library(Boost::redis ALIAS boost_redis) target_include_directories(boost_redis INTERFACE @@ -32,311 +49,182 @@ target_compile_features(boost_redis INTERFACE cxx_std_17) # Asio bases C++ feature detection on __cplusplus. Make MSVC # define it correctly if (MSVC) - target_compile_options(boost_redis INTERFACE /Zc:__cplusplus) + target_compile_options(boost_redis INTERFACE /Zc:__cplusplus) endif() -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - "${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.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/boost_redis.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) -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) -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() - -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() - -if (NOT MSVC) -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) -endif() - -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() - -if (NOT MSVC) -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) -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_json_serialization examples/cpp20_json_serialization.cpp) -target_compile_features(cpp20_json_serialization PUBLIC cxx_std_20) -target_link_libraries(cpp20_json_serialization common) -add_test(cpp20_json_serialization cpp20_json_serialization) -if (MSVC) - target_compile_options(cpp20_json_serialization PRIVATE /bigobj) - target_compile_definitions(cpp20_json_serialization PRIVATE _WIN32_WINNT=0x0601) -endif() - -if (NOT MSVC) -add_executable(cpp20_subscriber examples/cpp20_subscriber.cpp) -target_compile_features(cpp20_subscriber PUBLIC cxx_std_20) -target_link_libraries(cpp20_subscriber common) -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 tests/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 tests/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_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() +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(test_conn_exec_error tests/conn_exec_error.cpp) -target_compile_features(test_conn_exec_error PUBLIC cxx_std_17) -target_link_libraries(test_conn_exec_error common) -add_test(test_conn_exec_error test_conn_exec_error) -if (MSVC) - target_compile_options(test_conn_exec_error PRIVATE /bigobj) - target_compile_definitions(test_conn_exec_error PRIVATE _WIN32_WINNT=0x0601) -endif() + add_executable(echo_server_client benchmarks/cpp/asio/echo_server_client.cpp) + target_link_libraries(echo_server_client PRIVATE benchmarks_options) -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) + add_executable(echo_server_direct benchmarks/cpp/asio/echo_server_direct.cpp) + target_link_libraries(echo_server_direct PRIVATE benchmarks_options) 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) +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() -if (NOT MSVC) -add_executable(test_issue_50 tests/issue_50.cpp) -target_compile_features(test_issue_50 PUBLIC cxx_std_20) -target_link_libraries(test_issue_50 common) -add_test(test_issue_50 test_issue_50) -endif() - -if (NOT MSVC) -add_executable(test_conn_check_health tests/conn_check_health.cpp) -target_compile_features(test_conn_check_health PUBLIC cxx_std_17) -target_link_libraries(test_conn_check_health common) -add_test(test_conn_check_health test_conn_check_health) +if (BOOST_REDIS_TESTS) + enable_testing() + + add_library(tests_common STATIC tests/common.cpp) + target_compile_features(tests_common PRIVATE cxx_std_17) + target_link_libraries(tests_common PRIVATE boost_redis_project_options) + + macro(make_test TEST_NAME STANDARD) + add_executable(${TEST_NAME} tests/${TEST_NAME}.cpp) + target_link_libraries(${TEST_NAME} PRIVATE boost_redis_src tests_common) + target_link_libraries(${TEST_NAME} PRIVATE boost_redis_project_options) + target_compile_features(${TEST_NAME} PRIVATE cxx_std_${STANDARD}) + add_test(${TEST_NAME} ${TEST_NAME}) + endmacro() + + make_test(test_conn_quit 17) + make_test(test_conn_tls 17) + make_test(test_low_level 17) + make_test(test_conn_exec_retry 17) + make_test(test_conn_exec_error 17) + make_test(test_request 17) + make_test(test_run 17) + make_test(test_low_level_sync 17) + make_test(test_low_level_sync_sans_io 17) + make_test(test_conn_check_health 17) + + make_test(test_conn_exec 20) + make_test(test_conn_push 20) + make_test(test_conn_reconnect 20) + make_test(test_conn_exec_cancel 20) + make_test(test_conn_exec_cancel2 20) + make_test(test_conn_echo_stress 20) + make_test(test_low_level_async 20) + make_test(test_conn_run_cancel 20) + make_test(test_issue_50 20) endif() # Install #======================================================================= -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) +if (BOOST_REDIS_INSTALL) + install(TARGETS boost_redis + EXPORT boost_redis + PUBLIC_HEADER DESTINATION include COMPONENT Development + ) + + include(CMakePackageConfigHelpers) + + configure_package_config_file( + "${PROJECT_SOURCE_DIR}/cmake/BoostRedisConfig.cmake.in" + "${PROJECT_BINARY_DIR}/BoostRedisConfig.cmake" + INSTALL_DESTINATION lib/cmake/boost/redis + ) + + install(EXPORT boost_redis DESTINATION lib/cmake/boost/redis) + install(FILES "${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.cmake" + "${PROJECT_BINARY_DIR}/BoostRedisConfig.cmake" + DESTINATION lib/cmake/boost/redis) + + install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include) + + include(CMakePackageConfigHelpers) + write_basic_package_version_file( + "${PROJECT_BINARY_DIR}/BoostRedisConfigVersion.cmake" + COMPATIBILITY AnyNewerVersion + ) -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 #======================================================================= @@ -365,11 +253,6 @@ add_custom_target( VERBATIM ) -# Distribution -#======================================================================= - -include(CPack) - # TODO #======================================================================= diff --git a/CMakePresets.json b/CMakePresets.json index 7efcf951..c68fbf34 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -40,11 +40,11 @@ } }, { - "name": "g++-11-cpp17", + "name": "g++-11", "generator": "Unix Makefiles", "hidden": false, "inherits": ["cmake-pedantic"], - "binaryDir": "${sourceDir}/build/g++-11-cpp17", + "binaryDir": "${sourceDir}/build/g++-11", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_EXTENSIONS": "OFF", @@ -52,25 +52,42 @@ "CMAKE_CXX_COMPILER": "g++-11", "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address", "CMAKE_CXX_STANDARD_REQUIRED": "ON", - "PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-cpp17", - "DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11-cpp17/doc/" + "PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11", + "DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11/doc/" } }, { - "name": "g++-11-cpp20", + "name": "g++-11-release", "generator": "Unix Makefiles", "hidden": false, "inherits": ["cmake-pedantic"], - "binaryDir": "${sourceDir}/build/g++-11-cpp20", + "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": "g++-11", + "CMAKE_CXX_COMPILER": "clang++-13", "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address", "CMAKE_CXX_STANDARD_REQUIRED": "ON", - "PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-cpp20", - "DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/g++-11-cpp20/doc/" + "PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-13", + "DOXYGEN_OUTPUT_DIRECTORY": "${sourceDir}/build/clang++-13/doc/" } }, { @@ -113,7 +130,7 @@ "name": "clang-tidy", "generator": "Unix Makefiles", "hidden": false, - "inherits": ["g++-11-cpp17"], + "inherits": ["g++-11"], "binaryDir": "${sourceDir}/build/clang-tidy", "cacheVariables": { "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/include/*", @@ -123,8 +140,9 @@ ], "buildPresets": [ { "name": "coverage", "configurePreset": "coverage" }, - { "name": "g++-11-cpp17", "configurePreset": "g++-11-cpp17" }, - { "name": "g++-11-cpp20", "configurePreset": "g++-11-cpp20" }, + { "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" } @@ -136,10 +154,12 @@ "output": {"outputOnFailure": true}, "execution": {"noTestsAction": "error", "stopOnFailure": true} }, - { "name": "coverage", "configurePreset": "coverage", "inherits": ["test"] }, - { "name": "g++-11-cpp17", "configurePreset": "g++-11-cpp17", "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"] } + { "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"] } ] } diff --git a/README.md b/README.md index d2ce421e..cf1d4d59 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,76 @@ # boost_redis -Boost.Redis 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 -[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md), -a plain text protocol which can multiplex any number of client +that implements Redis plain text protocol +[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md). +It can multiplex any number of client requests, responses, and server pushes onto a single active socket -connection to the Redis server. The library hides low-level code away -from the user, which, in the majority of the cases will be concerned -with only three library entities - -* `boost::redis::connection`: A full-duplex connection to the Redis - server with high-level functions to execute Redis commands, receive - server pushes and automatic command [pipelines](https://redis.io/docs/manual/pipelining/). -* `boost::redis::request`: A container of Redis commands that supports - STL containers and user defined data types. -* `boost::redis::response`: Container of Redis responses. - -In the next sections we will cover all those points in detail with -examples. The requirements for using Boost.Redis are +connection to the Redis server. The requirements for using Boost.Redis are * 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). -To install Boost.Redis download the latest release on -https://github.com/boostorg/redis/releases. Boost.Redis is a header only -library, so you can starting using it right away by adding the -`include` subdirectory to your project and including +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 ```cpp #include ``` -in no more than one source file in your applications. To build the +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 dev +$ 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 ``` + ## Connection -Readers that are not familiar with Redis are advised to learn more about -it on https://redis.io/docs/ before we start, in essence - -> 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. - Let us start with a simple application that uses a short-lived connection to send a [ping](https://redis.io/commands/ping/) command to Redis ```cpp -auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { - // From examples/common.hpp to avoid vebosity - co_await connect(conn, host, port); - - // async_run coordinates read and write operations. - co_await conn->async_run(); - - // Cancel pending operations, if any. - conn->cancel(operation::exec); - conn->cancel(operation::receive); -} + auto conn = std::make_shared(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); -auto co_main(std::string host, std::string port) -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, host, port), net::detached); - - // A request can contain multiple commands. + // A request containing only a ping command. request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - req.push("QUIT"); - // Stores responses of each individual command. The responses to - // HELLO and QUIT are being ignored for simplicity. - response resp; + // Response where the PONG response will be stored. + response resp; // Executes the request. - co_await conn->async_exec(req, resp); + co_await conn->async_exec(req, resp, net::deferred); + conn->cancel(); - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } ``` + The roles played by the `async_run` and `async_exec` functions are -* `connection::async_exec`: Execute the commands contained in the +* `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. -* `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. - -Depending on the user's requirements, there are different styles of -calling `async_run`. For example, in a short-lived connection where -there is only one active client communicating with the server, the -easiest way to call `async_run` is to only run it simultaneously with -the `async_exec` call, this is exemplified in -cpp20_intro_awaitable_ops.cpp. If there are many in-process clients -performing simultaneous requests, an alternative is to launch a -long-running coroutine which calls `async_run` detached from other -operations as shown in the example above, cpp20_intro.cpp and -cpp20_echo_server.cpp. The list of examples below will help users -comparing different ways of implementing the ping example shown above - -* cpp20_intro_awaitable_ops.cpp: Uses awaitable operators. -* cpp20_intro.cpp: Calls `async_run` detached from other operations. -* 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`. - -While calling `async_run` is a sufficient condition for maintaining -active two-way communication with the Redis server, most production -deployments will want to do more. For example, they may want to -reconnect if the connection goes down, either to the same server or a -failover server. They may want to perform health checks and more. The -example below shows for example how to use a loop to keep reconnecting -to the same address when a disconnection occurs (see -cpp20_subscriber.cpp) - -```cpp -auto run(std::shared_ptr conn) -> net::awaitable -{ - steady_timer timer{co_await net::this_coro::executor}; - - for (;;) { - co_await connect(conn, "127.0.0.1", "6379"); - co_await (conn->async_run() || health_check(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(); - } -} -``` - -The ability to reconnect the same connection object 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. +* `async_run`: Resolve, connect, ssl-handshake, + resp3-handshake, health-checks, reconnection and coordinate low-level + read and write operations (among other things). ### Server pushes @@ -181,49 +86,35 @@ The connection class supports server pushes by means of the to used it ```cpp -auto receiver(std::shared_ptr conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - // Use resp and clear the response for a new push. - resp.clear(); - } -} -``` - -### Cancellation + request req; + req.push("SUBSCRIBE", "channel"); -Boost.Redis supports both implicit and explicit cancellation of connection -operations. Explicit cancellation is supported by means of the -`boost::redis::connection::cancel` member function. Implicit -terminal-cancellation, like those that happen when using Asio -awaitable `operator ||` will be discussed with more detail below. + // Loop while reconnection is enabled + while (conn->will_reconnect()) { -```cpp -co_await (conn.async_run(...) || conn.async_exec(...)) -``` + // Reconnect to channels. + co_await conn->async_exec(req, ignore, net::deferred); -* Useful for short-lived connections that are meant to be closed after - a command has been executed. + // 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. -```cpp -co_await (conn.async_exec(...) || time.async_wait(...)) -``` + // Use the response resp in some way and then clear it. + ... -* 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. + resp.value().clear(); + } + } +} -```cpp -co_await (conn.async_exec(...) || conn.async_exec(...) || ... || conn.async_exec(...)) ``` -* This works but is unnecessary, the connection will automatically - merge the individual requests into a single payload. - ## Requests @@ -254,22 +145,21 @@ req.push_range("HSET", "key", map); Sending a request to Redis is performed with `boost::redis::connection::async_exec` as already stated. - - ### Config flags 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. + ## Responses Boost.Redis uses the following strategy to support Redis responses -* **Static**: For `boost::redis::request` whose sizes are known at compile time use the `response` type. +* `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; @@ -278,18 +168,19 @@ 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 response ``` -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 -`boost::redis::ignore_t` +`boost::redis::ignore_t`, for example ```cpp // Ignore the second and last responses. @@ -353,18 +244,14 @@ response< Where both are passed to `async_exec` as showed elsewhere ```cpp -co_await conn->async_exec(req, resp); +co_await conn->async_exec(req, resp, net::deferred); ``` -If the intention is to ignore the response to all commands altogether -use `ignore` +If the intention is to ignore responses altogether use `ignore` ```cpp // Ignores the response -co_await conn->async_exec(req, ignore); - -// The default response argument will also ignore responses. -co_await conn->async_exec(req); +co_await conn->async_exec(req, ignore, net::deferred); ``` Responses that contain nested aggregates or heterogeneous data @@ -381,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; @@ -391,7 +278,7 @@ req.push("QUIT"); ``` must be read in this tuple `response`, -that has size two. +that has static size two. ### Null @@ -407,17 +294,17 @@ response< ... > resp; -co_await conn->async_exec(req, 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"); @@ -447,7 +334,7 @@ response< exec_resp_type, // exec > resp; -co_await conn->async_exec(req, resp); +co_await conn->async_exec(req, resp, net::deferred); ``` For a complete example see cpp20_containers.cpp. @@ -460,8 +347,8 @@ 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 `response`. @@ -495,7 +382,7 @@ using other types ```cpp // Receives any RESP3 simple or aggregate data type. boost::redis::generic_response resp; -co_await conn->async_exec(req, resp); +co_await conn->async_exec(req, resp, net::deferred); ``` For example, suppose we want to retrieve a hash data structure @@ -513,65 +400,33 @@ and other data structures in general. ## Serialization -Boost.Redis provides native support for serialization with Boost.Json. -To use it - -* Include boost/redis/serialization.hpp -* Describe your class with Boost.Describe. - -For example - -```cpp -#include - -struct user { - std::string name; - std::string age; - std::string country; -}; - -BOOST_DESCRIBE_STRUCT(user, (), (name, age, country)) -``` - -After that you will be able to user your described `struct` both in -requests and responses, for example +Boost.Redis supports serialization of user defined types by means of +the following customization points ```cpp -user foo{"Joao", "58", "Brazil"} -request req; -req.push("PING", foo); - -response resp; - -co_await conn->async_exec(req, resp); -``` - -For other serialization formats it is necessary to define the -serialization functions `boost_redis_to_bulk` and `boost_redis_from_bulk` and -import them onto the global namespace so they become available over -ADL. They must have the following signature - -```cpp - -// Serialize +// 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) ``` -Example cpp20_json_serialization.cpp shows how store json strings in Redis. +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/). + ## 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_json_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. @@ -579,9 +434,8 @@ The examples below show how to use the features discussed so far * 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 @@ -797,6 +651,7 @@ Acknowledgement to people that helped shape Boost.Redis * 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 @@ -819,12 +674,23 @@ https://lists.boost.org/Archives/boost/2023/01/253944.php. ## Changelog -### master (incorporates changes to conform the boost review and more) +### 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_buld` and `from_buld` names were too +* 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`. @@ -851,16 +717,16 @@ https://lists.boost.org/Archives/boost/2023/01/253944.php. became unnecessary and was removed. I could measure significative performance gains with theses changes. -* Adds native json support for Boost.Describe'd classes. To use it include - `` and decribe you class as of Boost.Describe, see - cpp20_json_serialization.cpp for more details. +* 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 a function that performs health checks, see - `boost::redis::experimental::async_check_health`. +* 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 diff --git a/examples/common/boost_redis.cpp b/examples/boost_redis.cpp similarity index 100% rename from examples/common/boost_redis.cpp rename to examples/boost_redis.cpp diff --git a/examples/common/common.cpp b/examples/common/common.cpp deleted file mode 100644 index 8b6725c7..00000000 --- a/examples/common/common.cpp +++ /dev/null @@ -1,68 +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 -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include - -namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using resolver = net::use_awaitable_t<>::as_default_on_t; -using timer_type = net::use_awaitable_t<>::as_default_on_t; -using boost::redis::request; -using boost::redis::operation; - -namespace -{ -auto redir(boost::system::error_code& ec) - { return net::redirect_error(net::use_awaitable, ec); } -} - -auto -connect( - std::shared_ptr conn, - std::string const& host, - std::string const& port) -> net::awaitable -{ - 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 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) diff --git a/examples/common/common.hpp b/examples/common/common.hpp deleted file mode 100644 index dc5a462c..00000000 --- a/examples/common/common.hpp +++ /dev/null @@ -1,33 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_EXAMPLES_COMMON_HPP -#define BOOST_REDIS_EXAMPLES_COMMON_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; - -auto -connect( - std::shared_ptr conn, - std::string const& host, - std::string const& port) -> boost::asio::awaitable; - -auto run(boost::asio::awaitable op) -> int; - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) -#endif // BOOST_REDIS_EXAMPLES_COMMON_HPP diff --git a/examples/common/main.cpp b/examples/common/main.cpp deleted file mode 100644 index 33b56d78..00000000 --- a/examples/common/main.cpp +++ /dev/null @@ -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 - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -#include "common.hpp" - -extern boost::asio::awaitable 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 - -auto main() -> int -{ - std::cout << "Requires coroutine support." << std::endl; - return 0; -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp17_intro.cpp b/examples/cpp17_intro.cpp index e1d34f18..13a303a0 100644 --- a/examples/cpp17_intro.cpp +++ b/examples/cpp17_intro.cpp @@ -4,99 +4,47 @@ * accompanying file LICENSE.txt) */ +#include +#include #include -#include -#include -#include namespace net = boost::asio; -namespace redis = boost::redis; -using redis::operation; -using redis::request; -using redis::response; -using redis::ignore_t; - -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 request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - req.push("QUIT"); - // The response. - response resp; + response resp; net::io_context ioc; + connection conn{ioc}; - // IO objects. - net::ip::tcp::resolver resv{ioc}; - redis::connection conn{ioc}; - - // 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: "); - } + conn.async_run(cfg, {}, net::detached); - std::cout << "PING: " << std::get<1>(resp).value() << 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, 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; } diff --git a/examples/cpp17_intro_sync.cpp b/examples/cpp17_intro_sync.cpp index e6ba8b96..e9a4627d 100644 --- a/examples/cpp17_intro_sync.cpp +++ b/examples/cpp17_intro_sync.cpp @@ -4,76 +4,40 @@ * accompanying file LICENSE.txt) */ -#include +#include "sync_connection.hpp" + #include -#include #include -#include -#include - -// Include this in no more than one .cpp file. -#include namespace net = boost::asio; -using connection = boost::redis::connection; +using boost::redis::sync_connection; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; - -template -auto exec(std::shared_ptr conn, request const& req, Response& resp) -{ - net::dispatch( - conn->get_executor(), - net::deferred([&]() { return conn->async_exec(req, resp, net::deferred); })) - (net::use_future).get(); -} - -auto logger = [](auto const& ec) - { std::clog << "Run: " << ec.message() << std::endl; }; +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}; - - auto conn = std::make_shared(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(); - }}; + sync_connection conn; + conn.run(cfg); request req; - req.push("HELLO", 3); req.push("PING"); - req.push("QUIT"); - response resp; + response resp; - // Executes commands synchronously. - exec(conn, req, resp); + conn.exec(req, resp); + conn.stop(); - std::cout << "Response: " << std::get<1>(resp).value() << 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; } diff --git a/examples/cpp20_chat_room.cpp b/examples/cpp20_chat_room.cpp index 744fa70a..24c4a025 100644 --- a/examples/cpp20_chat_room.cpp +++ b/examples/cpp20_chat_room.cpp @@ -4,35 +4,59 @@ * accompanying file LICENSE.txt) */ -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -namespace net = boost::asio; -#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include +#include -#include "common/common.hpp" +#if defined(BOOST_ASIO_HAS_CO_AWAIT) +#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) -using namespace net::experimental::awaitable_operators; -using stream_descriptor = net::use_awaitable_t<>::as_default_on_t; -using signal_set = net::use_awaitable_t<>::as_default_on_t; +namespace net = boost::asio; +using stream_descriptor = net::deferred_t::as_default_on_t; +using signal_set = net::deferred_t::as_default_on_t; using boost::redis::request; using boost::redis::generic_response; -using boost::redis::experimental::async_check_health; +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 conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " " << resp.value().at(3).value << std::endl; - resp.value().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(); + } } } @@ -42,32 +66,31 @@ auto publisher(std::shared_ptr in, std::shared_ptrasync_exec(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 +auto co_main(config cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); auto stream = std::make_shared(ex, ::dup(STDIN_FILENO)); - signal_set sig{ex, SIGINT, SIGTERM}; - 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) || - async_check_health(*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 +auto co_main(config const&) -> net::awaitable { std::cout << "Requires support for posix streams." << std::endl; co_return; diff --git a/examples/cpp20_containers.cpp b/examples/cpp20_containers.cpp index e29fc900..dfedd82e 100644 --- a/examples/cpp20_containers.cpp +++ b/examples/cpp20_containers.cpp @@ -4,21 +4,23 @@ * accompanying file LICENSE.txt) */ -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include +#include +#include +#include +#include #include #include +#include -#include "common/common.hpp" +#if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -namespace redis = boost::redis; -using namespace net::experimental::awaitable_operators; 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 const& cont) { @@ -32,12 +34,6 @@ void print(std::vector const& cont) std::cout << "\n"; } -auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable -{ - 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 conn) -> net::awaitable { @@ -48,71 +44,59 @@ auto store(std::shared_ptr conn) -> net::awaitable {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}; request req; - req.push("HELLO", 3); 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 conn) -> net::awaitable { // A request contains multiple commands. request req; - req.push("HELLO", 3); req.push("HGETALL", "hset-key"); // Responses as tuple elements. - response> resp; + response> resp; // Executes the request and reads the response. - co_await conn->async_exec(req, resp); + co_await conn->async_exec(req, resp, net::deferred); - print(std::get<1>(resp).value()); + print(std::get<0>(resp).value()); } // Retrieves in a transaction. auto transaction(std::shared_ptr conn) -> net::awaitable { request req; - req.push("HELLO", 3); req.push("MULTI"); req.push("LRANGE", "rpush-key", 0, -1); // Retrieves req.push("HGETALL", "hset-key"); // Retrieves req.push("EXEC"); response< - ignore_t, // hello ignore_t, // multi ignore_t, // lrange ignore_t, // hgetall response>, std::optional>> // exec > resp; - co_await conn->async_exec(req, resp); + co_await conn->async_exec(req, resp, net::deferred); - print(std::get<0>(std::get<4>(resp).value()).value().value()); - print(std::get<1>(std::get<4>(resp).value()).value().value()); -} - -auto quit(std::shared_ptr conn) -> net::awaitable -{ - 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 co_main(std::string host, std::string port) +net::awaitable co_main(config cfg) { - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, host, port), net::detached); + auto conn = std::make_shared(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) diff --git a/examples/cpp20_echo_server.cpp b/examples/cpp20_echo_server.cpp index 292f7098..9b637240 100644 --- a/examples/cpp20_echo_server.cpp +++ b/examples/cpp20_echo_server.cpp @@ -4,21 +4,26 @@ * accompanying file LICENSE.txt) */ -#include +#include +#include +#include +#include +#include +#include +#include + #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include -#include -#include "common/common.hpp" namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using tcp_socket = net::use_awaitable_t<>::as_default_on_t; -using tcp_acceptor = net::use_awaitable_t<>::as_default_on_t; -using signal_set = net::use_awaitable_t<>::as_default_on_t; +using tcp_socket = net::deferred_t::as_default_on_t; +using tcp_acceptor = net::deferred_t::as_default_on_t; +using signal_set = net::deferred_t::as_default_on_t; using boost::redis::request; using boost::redis::response; -using boost::redis::experimental::async_check_health; +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 conn) -> net::awaitable { @@ -28,7 +33,7 @@ auto echo_server_session(tcp_socket socket, std::shared_ptr conn) -> for (std::string buffer;;) { auto n = co_await net::async_read_until(socket, net::dynamic_buffer(buffer, 1024), "\n"); req.push("PING", buffer); - co_await conn->async_exec(req, resp); + 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(); @@ -39,25 +44,27 @@ auto echo_server_session(tcp_socket socket, std::shared_ptr conn) -> // Listens for tcp connections. auto listener(std::shared_ptr conn) -> net::awaitable { - 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 +auto co_main(config cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - signal_set sig{ex, SIGINT, SIGTERM}; - - request req; - req.push("HELLO", 3); + net::co_spawn(ex, listener(conn), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - co_await connect(conn, host, port); - co_await ((conn->async_run() || listener(conn) || async_check_health(*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) diff --git a/examples/cpp20_intro.cpp b/examples/cpp20_intro.cpp index 1dc92079..195122cf 100644 --- a/examples/cpp20_intro.cpp +++ b/examples/cpp20_intro.cpp @@ -4,51 +4,39 @@ * accompanying file LICENSE.txt) */ -#include +#include +#include +#include +#include +#include +#include + #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include "common/common.hpp" namespace net = boost::asio; -using boost::redis::operation; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; - -auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable -{ - // From examples/common.hpp to avoid vebosity - co_await connect(conn, host, port); - - // async_run coordinate read and write operations. - co_await conn->async_run(); - - // Cancel pending operations, if any. - conn->cancel(operation::exec); - conn->cancel(operation::receive); -} +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 +auto co_main(config cfg) -> net::awaitable { - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, host, port), net::detached); + auto conn = std::make_shared(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - // A request can contain multiple commands. + // A request containing only a ping command. request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - req.push("QUIT"); - // Stores responses of each individual command. The responses to - // HELLO and QUIT are being ignored for simplicity. - response resp; + // Response where the PONG response will be stored. + response resp; - // Executtes the request. - co_await conn->async_exec(req, resp); + // Executes the request. + co_await conn->async_exec(req, resp, net::deferred); + conn->cancel(); - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_intro_awaitable_ops.cpp b/examples/cpp20_intro_awaitable_ops.cpp deleted file mode 100644 index 7759fbc2..00000000 --- a/examples/cpp20_intro_awaitable_ops.cpp +++ /dev/null @@ -1,40 +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 -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include -#include "common/common.hpp" - -namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; - -// Called from the main function (see main.cpp) -auto co_main(std::string host, std::string port) -> net::awaitable -{ - try { - request req; - req.push("HELLO", 3); - req.push("PING", "Hello world"); - req.push("QUIT"); - - response resp; - - auto conn = std::make_shared(co_await net::this_coro::executor); - co_await connect(conn, host, port); - co_await (conn->async_run() || conn->async_exec(req, resp)); - - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; - } catch (std::exception const& e) { - std::cout << e.what() << std::endl; - } -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_intro_tls.cpp b/examples/cpp20_intro_tls.cpp index 5d607337..b911af27 100644 --- a/examples/cpp20_intro_tls.cpp +++ b/examples/cpp20_intro_tls.cpp @@ -4,26 +4,21 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include +#include +#include +#include #include -#include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include - -#include -#include namespace net = boost::asio; -namespace redis = boost::redis; -using namespace net::experimental::awaitable_operators; -using resolver = net::use_awaitable_t<>::as_default_on_t; -using connection = net::use_awaitable_t<>::as_default_on_t; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; +using boost::redis::config; +using boost::redis::logger; +using boost::redis::connection; auto verify_certificate(bool, net::ssl::verify_context&) -> bool { @@ -31,30 +26,29 @@ auto verify_certificate(bool, net::ssl::verify_context&) -> bool return true; } -net::awaitable co_main(std::string, std::string) +auto co_main(config cfg) -> net::awaitable { + 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(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); + request req; - req.push("HELLO", 3, "AUTH", "aedis", "aedis"); req.push("PING"); - req.push("QUIT"); - - response 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"); + response resp; - 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); + conn->next_layer().set_verify_mode(net::ssl::verify_peer); + conn->next_layer().set_verify_callback(verify_certificate); - 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, resp)); + co_await conn->async_exec(req, resp, net::deferred); + conn->cancel(); - std::cout << "Response: " << std::get<1>(resp).value() << std::endl; + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_json.cpp b/examples/cpp20_json.cpp new file mode 100644 index 00000000..d0c6423c --- /dev/null +++ b/examples/cpp20_json.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#define BOOST_JSON_NO_LIB +#define BOOST_CONTAINER_NO_LIB +#include +#include +#include +#include +#include + +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(boost::json::parse(sv)); } + +auto co_main(config cfg) -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(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 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) diff --git a/examples/cpp20_json_serialization.cpp b/examples/cpp20_json_serialization.cpp deleted file mode 100644 index c47171a8..00000000 --- a/examples/cpp20_json_serialization.cpp +++ /dev/null @@ -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 -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#define BOOST_JSON_NO_LIB -#define BOOST_CONTAINER_NO_LIB -#include -#include -#include -#include -#include -#include -#include "common/common.hpp" - -// Include this in no more than one .cpp file. -#include - -namespace net = boost::asio; -namespace redis = boost::redis; -using namespace boost::describe; -using boost::redis::request; -using boost::redis::response; -using boost::redis::operation; -using boost::redis::ignore_t; - -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); } -}; - -BOOST_DESCRIBE_STRUCT(user, (), (name, age, country)) - -auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable -{ - co_await connect(conn, host, port); - co_await conn->async_run(); -} - -net::awaitable co_main(std::string host, std::string port) -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, host, port), net::detached); - - // A set of users that will be automatically serialized to json. - std::set users - {{"Joao", "58", "Brazil"} , {"Serge", "60", "France"}}; - - // To simplify we send the set and retrieve it in the same - // resquest. - request req; - req.push("HELLO", 3); - - // Stores a std::set in a Redis set data structure. - req.push_range("SADD", "sadd-key", users); - - // Sends a ping and retrieves it as a string to show what json - // serialization looks like. - req.push("PING", *users.begin()); - - // Sends another ping and retrieves it directly in a user type. - req.push("PING", *users.begin()); - - // Retrieves the set we have just stored. - req.push("SMEMBERS", "sadd-key"); - - response> resp; - - // Sends the request and receives the response. - co_await conn->async_exec(req, resp); - - // Prints the first ping - auto const& pong1 = std::get<2>(resp).value(); - std::cout << pong1 << "\n"; - - // Prints the second ping. - auto const& pong2 = std::get<3>(resp).value(); - std::cout << pong2.name << " " << pong2.age << " " << pong2.country << "\n"; - - // Prints the set. - for (auto const& e: std::get<4>(resp).value()) - std::cout << e.name << " " << e.age << " " << e.country << "\n"; - - conn->cancel(operation::run); -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_protobuf.cpp b/examples/cpp20_protobuf.cpp new file mode 100644 index 00000000..75eb8fd2 --- /dev/null +++ b/examples/cpp20_protobuf.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +// 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 co_main(config cfg) +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(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 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) diff --git a/examples/cpp20_resolve_with_sentinel.cpp b/examples/cpp20_resolve_with_sentinel.cpp index d461513d..8401cd1e 100644 --- a/examples/cpp20_resolve_with_sentinel.cpp +++ b/examples/cpp20_resolve_with_sentinel.cpp @@ -4,32 +4,30 @@ * accompanying file LICENSE.txt) */ -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include +#include +#include +#include +#include +#include -#include "common/common.hpp" +#if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; using endpoints = net::ip::tcp::resolver::results_type; using boost::redis::request; using boost::redis::response; using boost::redis::ignore_t; +using boost::redis::config; +using boost::redis::address; +using boost::redis::connection; auto redir(boost::system::error_code& ec) { return 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
const& endpoints) -> net::awaitable
+auto resolve_master_address(std::vector
const& addresses) -> net::awaitable
{ request req; req.push("SENTINEL", "get-master-addr-by-name", "mymaster"); @@ -37,30 +35,36 @@ auto resolve_master_address(std::vector
const& endpoints) -> net::await auto conn = std::make_shared(co_await net::this_coro::executor); - response>, ignore_t> addr; - for (auto ep : endpoints) { + response>, 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, 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().value().at(0), std::get<0>(addr).value().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 +auto co_main(config cfg) -> net::awaitable { // A list of sentinel addresses from which only one is responsive. // This simulates sentinels that are down. - std::vector
const endpoints - { {"foo", "26379"} - , {"bar", "26379"} - , {host, port} + std::vector
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" diff --git a/examples/cpp20_streams.cpp b/examples/cpp20_streams.cpp new file mode 100644 index 00000000..ae1d0206 --- /dev/null +++ b/examples/cpp20_streams.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include +#include +#include +#include + +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; + +auto stream_reader(std::shared_ptr conn) -> net::awaitable +{ + 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 +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(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) diff --git a/examples/cpp20_subscriber.cpp b/examples/cpp20_subscriber.cpp index 44f9c953..69884705 100644 --- a/examples/cpp20_subscriber.cpp +++ b/examples/cpp20_subscriber.cpp @@ -4,20 +4,30 @@ * accompanying file LICENSE.txt) */ -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include "common/common.hpp" +#if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using steady_timer = net::use_awaitable_t<>::as_default_on_t; +using namespace std::chrono_literals; using boost::redis::request; using boost::redis::generic_response; -using boost::redis::experimental::async_check_health; +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; /* This example will subscribe and read pushes indefinitely. * @@ -35,35 +45,46 @@ using boost::redis::experimental::async_check_health; * > CLIENT kill TYPE pubsub */ -// Receives pushes. -auto receiver(std::shared_ptr conn) -> net::awaitable +// Receives server pushes. +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " " << resp.value().at(3).value << std::endl; - resp.value().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 +auto co_main(config cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - steady_timer timer{ex}; + net::co_spawn(ex, receiver(conn), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - 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() || async_check_health(*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) diff --git a/examples/main.cpp b/examples/main.cpp new file mode 100644 index 00000000..78e0a56a --- /dev/null +++ b/examples/main.cpp @@ -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 +#include +#include +#include +#include +#include + +namespace net = boost::asio; +using boost::redis::config; +using boost::redis::logger; + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +extern net::awaitable 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) diff --git a/examples/person.proto b/examples/person.proto new file mode 100644 index 00000000..bec5b2c2 --- /dev/null +++ b/examples/person.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package tutorial; + +message person { + optional string name = 1; + optional int32 id = 2; + optional string email = 3; +} diff --git a/examples/sync_connection.hpp b/examples/sync_connection.hpp new file mode 100644 index 00000000..cc982f84 --- /dev/null +++ b/examples/sync_connection.hpp @@ -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 +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace boost::redis +{ + +class sync_connection { +public: + sync_connection() + : ioc_{1} + , conn_{std::make_shared(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 + 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 conn_; + std::thread thread_; +}; + +} diff --git a/include/boost/redis.hpp b/include/boost/redis.hpp index 0972fe0c..7d3272b8 100644 --- a/include/boost/redis.hpp +++ b/include/boost/redis.hpp @@ -7,10 +7,13 @@ #ifndef BOOST_REDIS_HPP #define BOOST_REDIS_HPP +#include #include #include #include #include +#include +#include /** @defgroup high-level-api Reference * diff --git a/include/boost/redis/adapter/detail/adapters.hpp b/include/boost/redis/adapter/detail/adapters.hpp index 46705bb2..43bf6866 100644 --- a/include/boost/redis/adapter/detail/adapters.hpp +++ b/include/boost/redis/adapter/detail/adapters.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include diff --git a/include/boost/redis/adapter/detail/result_traits.hpp b/include/boost/redis/adapter/detail/result_traits.hpp index 2cc6afc9..09c3b520 100644 --- a/include/boost/redis/adapter/detail/result_traits.hpp +++ b/include/boost/redis/adapter/detail/result_traits.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/include/boost/redis/adapter/ignore.hpp b/include/boost/redis/adapter/ignore.hpp index a468e966..29a5502c 100644 --- a/include/boost/redis/adapter/ignore.hpp +++ b/include/boost/redis/adapter/ignore.hpp @@ -8,6 +8,7 @@ #define BOOST_REDIS_ADAPTER_IGNORE_HPP #include +#include #include #include diff --git a/include/boost/redis/adapter/result.hpp b/include/boost/redis/adapter/result.hpp index d36e3ca9..a1f29f0e 100644 --- a/include/boost/redis/adapter/result.hpp +++ b/include/boost/redis/adapter/result.hpp @@ -9,6 +9,7 @@ #define BOOST_REDIS_ADAPTER_RESULT_HPP #include +#include #include #include diff --git a/include/boost/redis/config.hpp b/include/boost/redis/config.hpp new file mode 100644 index 00000000..f2b1c8e8 --- /dev/null +++ b/include/boost/redis/config.hpp @@ -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 +#include +#include + +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 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 diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 8c03ab80..a3d573ab 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -8,83 +8,129 @@ #define BOOST_REDIS_CONNECTION_HPP #include -#include +#include +#include #include +#include +#include +#include +#include #include #include +#include namespace boost::redis { +namespace detail +{ +template +struct reconnection_op { + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; -/** @brief A connection to the Redis server. + template + 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 * - * For more details, please see the documentation of each individual - * function. + * 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 basic_connection : - private detail::connection_base< - typename Socket::executor_type, - basic_connection> { +template +class basic_connection { public: /// Executor type. - using executor_type = typename Socket::executor_type; + using executor_type = Executor; - /// Type of the next layer - using next_layer_type = Socket; + /// Returns the underlying executor. + executor_type get_executor() noexcept + { return impl_.get_executor(); } /// Rebinds the socket type to another executor. template struct rebind_executor { - /// The socket type when rebound to the specified executor. - using other = basic_connection::other>; + /// The connection type when rebound to the specified executor. + using other = basic_connection; }; - using base_type = detail::connection_base>; - /// Contructs from an executor. explicit - basic_connection(executor_type ex) - : base_type{ex} - , stream_{ex} - {} + basic_connection( + executor_type ex, + asio::ssl::context::method method = asio::ssl::context::tls_client, + std::size_t max_read_size = (std::numeric_limits::max)()) + : impl_{ex, method, max_read_size} + , timer_{ex} + { } /// Contructs from a context. explicit - basic_connection(asio::io_context& ioc) - : basic_connection(ioc.get_executor()) + 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::max)()) + : basic_connection(ioc.get_executor(), method, max_read_size) { } - /// Returns the associated executor. - auto get_executor() {return stream_.get_executor();} - - /// Resets the underlying stream. - void reset_stream() - { - if (stream_.is_open()) { - system::error_code ignore; - stream_.shutdown(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 + /** @brief Starts underlying connection operations. + * + * This member function provides the following functionality * - * This function starts read and write operations with the Redis + * 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 @@ -93,107 +139,97 @@ class basic_connection : * void f(system::error_code); * @endcode * - * @remarks - * - * * This function will complete only when the connection is lost. - * If the error is asio::error::eof this function will complete - * without error. - * * It can can be called multiple times on the same connection - * object. This makes it simple to implement reconnection in a way - * that does not require cancelling any pending connections. - * - * For examples of how to call this function see the examples. For - * example, if reconnection is not necessary, the coroutine below - * is enough - * - * ```cpp - * auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable - * { - * // From examples/common.hpp to avoid vebosity - * co_await connect(conn, host, port); - * - * // async_run coordinate read and write operations. - * co_await conn->async_run(); - * - * // Cancel pending operations, if any. - * conn->cancel(operation::exec); - * conn->cancel(operation::receive); - * } - * ``` - * - * For a reconnection example see cpp20_subscriber.cpp. + * For example on how to call this function refer to + * cpp20_intro.cpp or any other example. */ - template > - auto async_run(CompletionToken token = CompletionToken{}) + template < + class Logger = logger, + class CompletionToken = asio::default_completion_token_t> + auto + async_run( + config const& cfg = {}, + Logger l = Logger{}, + CompletionToken token = CompletionToken{}) { - return base_type::async_run(std::move(token)); + using this_type = basic_connection; + + cfg_ = cfg; + l.set_prefix(cfg_.log_prefix); + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(detail::reconnection_op{this, l}, token, timer_); } - /** @brief Executes a command on the Redis server asynchronously. + /** @brief Receives server side pushes 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. + * 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 req Request object. * @param response Response object. - * @param token Asio completion token. + * @param token Completion token. * - * For an example see cpp20_echo_server.cpp. The completion token must + * 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 response in + * 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> - auto async_exec( - request const& req, - Response& response = ignore, + class CompletionToken = asio::default_completion_token_t + > + auto + async_receive( + Response& response, CompletionToken token = CompletionToken{}) { - return base_type::async_exec(req, response, std::move(token)); + return impl_.async_receive(response, token); } - /** @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. + /** @brief Executes commands on the Redis server asynchronously. * - * To cancel an ongoing receive operation apps should call - * `connection::cancel(operation::receive)`. + * 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 response The response object. - * @param token The Asio completion token. + * @param req Request. + * @param resp Response. + * @param token Completion token. * - * For an example see cpp20_subscriber.cpp. The completion token must + * 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 push in - * bytes. + * 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> - auto async_receive( - Response& response = ignore, - CompletionToken token = CompletionToken{}) + class CompletionToken = asio::default_completion_token_t + > + auto + async_exec( + request const& req, + Response& resp = ignore, + CompletionToken&& token = CompletionToken{}) { - return base_type::async_receive(response, std::move(token)); + return impl_.async_exec(req, resp, std::forward(token)); } /** @brief Cancel operations. @@ -201,55 +237,151 @@ class basic_connection : * @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`. + * @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. */ - auto cancel(operation op) -> std::size_t - { return base_type::cancel(op); } + 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 */; + } - /// Sets the maximum size of the read buffer. - void set_max_buffer_read_size(std::size_t max_read_size) noexcept - { base_type::set_max_buffer_read_size(max_read_size); } + impl_.cancel(op); + } - /** @brief Reserve memory on the read and write internal buffers. - * - * This function will call `std::string::reserve` on the - * underlying buffers. - * - * @param read The new capacity of the read buffer. - * @param write The new capacity of the write buffer. - */ - void reserve(std::size_t read, std::size_t write) - { base_type::reserve(read, write); } + /// 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 this_type = basic_connection; - - template friend class detail::connection_base; - template friend struct detail::exec_read_op; - template friend struct detail::exec_op; - template friend struct detail::receive_op; - template friend struct detail::reader_op; - template friend struct detail::writer_op; - template friend struct detail::run_op; - template friend struct detail::wait_receive_op; - - void close() { stream_.close(); } - auto is_open() const noexcept { return stream_.is_open(); } - auto lowest_layer() noexcept -> auto& { return stream_.lowest_layer(); } - - Socket stream_; + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + template friend struct detail::reconnection_op; + + config cfg_; + detail::connection_base impl_; + timer_type timer_; }; -/** \brief A connection that uses a asio::ip::tcp::socket. +/** \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`. */ -using connection = 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::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::max)()); + + /// Returns the underlying executor. + executor_type get_executor() noexcept + { return impl_.get_executor(); } + + /// Calls `boost::redis::basic_connection::async_run`. + template + 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 + auto async_receive(Response& response, CompletionToken token) + { + return impl_.async_receive(response, std::move(token)); + } + + /// Calls `boost::redis::basic_connection::async_exec`. + template + 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 token); + + basic_connection impl_; +}; } // boost::redis diff --git a/include/boost/redis/detail/connection_base.hpp b/include/boost/redis/detail/connection_base.hpp index 4e63f72a..f2f5f35e 100644 --- a/include/boost/redis/detail/connection_base.hpp +++ b/include/boost/redis/detail/connection_base.hpp @@ -8,123 +8,548 @@ #define BOOST_REDIS_CONNECTION_BASE_HPP #include +#include +#include +#include #include #include -#include -#include +#include +#include +#include + +#include +#include +#include +#include #include #include -#include -#include +#include +#include +#include +#include +#include +#include -#include -#include -#include +#include +#include #include +#include #include +#include #include namespace boost::redis::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 connection_base { +template +struct wait_receive_op { + Conn* conn_; + asio::coroutine coro{}; + + template + 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 read_next_op { public: - using executor_type = Executor; - using this_type = connection_base; + using req_info_type = typename Conn::req_info; + using req_info_ptr = typename std::shared_ptr; - connection_base(executor_type ex) - : writer_timer_{ex} - , read_timer_{ex} - , channel_{ex} +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 { - writer_timer_.expires_at(std::chrono::steady_clock::time_point::max()); - read_timer_.expires_at(std::chrono::steady_clock::time_point::max()); + return [i = index_, adpt = adapter_] (resp3::basic_node const& nd, system::error_code& ec) mutable { adpt(i, nd, ec); }; } - auto get_executor() {return writer_timer_.get_executor();} + template + 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_); + } + } +}; - auto cancel(operation op) -> std::size_t +template +struct receive_op { + Conn* conn_; + Adapter adapter; + asio::coroutine coro{}; + + template + void + operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) { - switch (op) { - case operation::exec: - { - return cancel_unwritten_requests(); + 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)); } - case operation::run: - { - derived().close(); - read_timer_.cancel(); - writer_timer_.cancel(); - cancel_on_conn_lost(); - return 1U; + if (ec || is_cancelled(self)) { + conn_->cancel(operation::run); + conn_->cancel(operation::receive); + self.complete(!!ec ? ec : asio::error::operation_aborted, {}); + return; } - case operation::receive: - { - channel_.cancel(); - return 1U; + + conn_->dbuf_.consume(n); + + if (!conn_->is_next_push()) { + conn_->read_op_timer_.cancel(); } - default: BOOST_ASSERT(false); return 0; + + self.complete({}, n); + return; } } +}; - auto cancel_unwritten_requests() -> std::size_t +template +struct exec_op { + using req_info_type = typename Conn::req_info; + + Conn* conn = nullptr; + request const* req = nullptr; + Adapter adapter{}; + std::shared_ptr info = nullptr; + asio::coroutine coro{}; + + template + void + operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) { - auto f = [](auto const& ptr) + BOOST_ASIO_CORO_REENTER (coro) { - BOOST_ASSERT(ptr != nullptr); - return ptr->is_written(); - }; + // 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); + } - auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), f); + info = std::allocate_shared(asio::get_associated_allocator(self), *req, conn->get_executor()); - auto const ret = std::distance(point, std::end(reqs_)); + 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); - std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->stop(); - }); + if (info->stop_requested()) { + // Don't have to call remove_request as it has already + // been by cancel(exec). + return self.complete(ec, 0); + } - reqs_.erase(point, std::end(reqs_)); - return ret; + 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); + } } +}; - auto cancel_on_conn_lost() -> std::size_t +template +struct run_op { + Conn* conn = nullptr; + Logger logger_; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array order = {} + , system::error_code ec0 = {} + , system::error_code ec1 = {}) { - // Must return false if the request should be removed. - auto cond = [](auto const& ptr) + BOOST_ASIO_CORO_REENTER (coro) { - BOOST_ASSERT(ptr != nullptr); + 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; + } - if (ptr->is_written()) { - return !ptr->get_request().get_config().cancel_if_unresponded; + switch (order[0]) { + case 0: self.complete(ec0); break; + case 1: self.complete(ec1); break; + default: BOOST_ASSERT(false); + } + } + } +}; + +template +struct writer_op { + Conn* conn_; + Logger logger_; + asio::coroutine coro{}; + + template + 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 +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 + 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 { - return !ptr->get_request().get_config().cancel_on_connection_lost; + 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 = {}; } - }; - auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond); + if (!conn->is_open() || ec || is_cancelled(self)) { + conn->cancel(operation::run); + self.complete(asio::error::basic_errors::operation_aborted); + return; + } + } + } +}; - auto const ret = std::distance(point, std::end(reqs_)); +/** @brief Base class for high level Redis asynchronous connections. + * @ingroup high-level-api + * + * @tparam Executor The executor type. + * + */ +template +class connection_base { +public: + /// Executor type + using executor_type = Executor; - std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->stop(); - }); + /// Type of the next layer + using next_layer_type = asio::ssl::stream>; - reqs_.erase(point, std::end(reqs_)); - std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) { - return ptr->reset_status(); - }); + using this_type = connection_base; - return ret; + /// 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(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(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 @@ -137,7 +562,7 @@ class connection_base { return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(redis::detail::exec_op{&derived(), &req, f}, token, writer_timer_); + >(redis::detail::exec_op{this, &req, f}, token, writer_timer_); } template @@ -150,35 +575,97 @@ class connection_base { return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(redis::detail::receive_op{&derived(), f}, token, channel_); + >(redis::detail::receive_op{this, f}, token, read_op_timer_); } - template - auto async_run(CompletionToken token) + template + auto async_run(config const& cfg, Logger l, CompletionToken token) { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(detail::run_op{&derived()}, token, writer_timer_); - } - - void set_max_buffer_read_size(std::size_t max_read_size) noexcept - {max_read_size_ = max_read_size;} - - // Reserves memory in the read and write buffer. - void reserve(std::size_t read, std::size_t write) - { - read_buffer_.reserve(read); - write_buffer_.reserve(write); + 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; using timer_type = asio::basic_waitable_timer; - using channel_type = asio::experimental::channel; + using runner_type = redis::detail::runner; + + 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; + } - auto derived() -> Derived& { return static_cast(*this); } + 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() { @@ -283,13 +770,14 @@ class connection_base { using reqs_type = std::deque>; - template friend struct detail::reader_op; - template friend struct detail::writer_op; - template friend struct detail::run_op; - template friend struct detail::exec_op; - template friend struct detail::exec_read_op; - template friend struct detail::receive_op; - template friend struct detail::wait_receive_op; + template friend struct redis::detail::reader_op; + template friend struct redis::detail::writer_op; + template friend struct redis::detail::run_op; + template friend struct redis::detail::exec_op; + template friend class redis::detail::read_next_op; + template friend struct redis::detail::receive_op; + template friend struct redis::detail::wait_receive_op; + template friend struct redis::detail::run_all_op; template auto async_wait_receive(CompletionToken token) @@ -297,7 +785,7 @@ class connection_base { return asio::async_compose < CompletionToken , void(system::error_code) - >(wait_receive_op{&derived()}, token, channel_); + >(redis::detail::wait_receive_op{this}, token, read_op_timer_); } void cancel_push_requests() @@ -330,38 +818,46 @@ class connection_base { std::rotate(std::rbegin(reqs_), std::rbegin(reqs_) + 1, rend); } - if (derived().is_open() && !is_writing()) + if (is_open() && !is_writing()) writer_timer_.cancel(); } - auto make_dynamic_buffer() - { return asio::dynamic_buffer(read_buffer_, max_read_size_); } - template auto reader(CompletionToken&& token) { return asio::async_compose < CompletionToken , void(system::error_code) - >(detail::reader_op{&derived()}, token, writer_timer_); + >(redis::detail::reader_op{this}, token, writer_timer_); } - template - auto writer(CompletionToken&& token) + template + auto writer(Logger l, CompletionToken&& token) { return asio::async_compose < CompletionToken , void(system::error_code) - >(detail::writer_op{&derived()}, token, writer_timer_); + >(redis::detail::writer_op{this, l}, token, writer_timer_); } template - auto async_exec_read(Adapter adapter, std::size_t cmds, CompletionToken token) + auto async_read_next(Adapter adapter, CompletionToken token) { return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(detail::exec_read_op{&derived(), adapter, cmds}, token, writer_timer_); + >(redis::detail::read_next_op{*this, adapter, reqs_.front()}, token, writer_timer_); + } + + template + 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, l}, token, writer_timer_); } [[nodiscard]] bool coalesce_requests() @@ -386,17 +882,37 @@ class connection_base { 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 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_; - channel_type channel_; + timer_type read_op_timer_; + runner_type runner_; + + using dyn_buffer_type = asio::dynamic_string_buffer, std::allocator>; std::string read_buffer_; + dyn_buffer_type dbuf_; std::string write_buffer_; reqs_type reqs_; - std::size_t max_read_size_ = (std::numeric_limits::max)(); }; } // boost::redis::detail diff --git a/include/boost/redis/detail/connection_ops.hpp b/include/boost/redis/detail/connection_ops.hpp deleted file mode 100644 index 36ee4336..00000000 --- a/include/boost/redis/detail/connection_ops.hpp +++ /dev/null @@ -1,402 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_CONNECTION_OPS_HPP -#define BOOST_REDIS_CONNECTION_OPS_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace boost::redis::detail { - -template -struct wait_receive_op { - Conn* conn; - asio::coroutine coro{}; - - template - void - operator()(Self& self , system::error_code ec = {}) - { - BOOST_ASIO_CORO_REENTER (coro) - { - BOOST_ASIO_CORO_YIELD - conn->channel_.async_send(system::error_code{}, 0, std::move(self)); - AEDIS_CHECK_OP0(;); - - BOOST_ASIO_CORO_YIELD - conn->channel_.async_send(system::error_code{}, 0, std::move(self)); - AEDIS_CHECK_OP0(;); - - self.complete({}); - } - } -}; - -template -struct exec_read_op { - Conn* conn; - Adapter adapter; - std::size_t cmds = 0; - std::size_t read_size = 0; - std::size_t index = 0; - asio::coroutine coro{}; - - template - 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. - BOOST_ASSERT(!conn->reqs_.empty()); - while (cmds != 0) { - BOOST_ASSERT(conn->is_waiting_response()); - - //----------------------------------- - // 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 - 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->async_wait_receive(std::move(self)); - AEDIS_CHECK_OP1(conn->cancel(operation::run);); - continue; - } - //----------------------------------- - - BOOST_ASIO_CORO_YIELD - redis::detail::async_read( - conn->next_layer(), - conn->make_dynamic_buffer(), - [i = index, adpt = adapter] (resp3::basic_node const& nd, 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; - } - - self.complete({}, read_size); - } - } -}; - -template -struct receive_op { - Conn* conn; - Adapter adapter; - std::size_t read_size = 0; - asio::coroutine coro{}; - - template - void - operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - BOOST_ASIO_CORO_REENTER (coro) - { - BOOST_ASIO_CORO_YIELD - conn->channel_.async_receive(std::move(self)); - AEDIS_CHECK_OP1(;); - - BOOST_ASIO_CORO_YIELD - redis::detail::async_read(conn->next_layer(), conn->make_dynamic_buffer(), 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; - } - - read_size = n; - - BOOST_ASIO_CORO_YIELD - conn->channel_.async_receive(std::move(self)); - AEDIS_CHECK_OP1(;); - - self.complete({}, read_size); - return; - } - } -}; - -template -struct exec_op { - using req_info_type = typename Conn::req_info; - - Conn* conn = nullptr; - request const* req = nullptr; - Adapter adapter{}; - std::shared_ptr info = nullptr; - std::size_t read_size = 0; - asio::coroutine coro{}; - - template - 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()) { - return self.complete(error::not_connected, 0); - } - - info = std::allocate_shared(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_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->is_waiting_response()) { - BOOST_ASSERT(!conn->reqs_.empty()); - conn->reqs_.front()->proceed(); - } else { - conn->read_timer_.cancel_one(); - } - - self.complete({}, read_size); - } - } -}; - -template -struct run_op { - Conn* conn = nullptr; - asio::coroutine coro{}; - - template - void operator()( Self& self - , std::array 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(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 -struct writer_op { - Conn* conn; - asio::coroutine coro{}; - - template - 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()) { - BOOST_ASIO_CORO_YIELD - asio::async_write(conn->next_layer(), 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 -struct reader_op { - Conn* conn; - asio::coroutine coro{}; - - template - void operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - ignore_unused(n); - - BOOST_ASIO_CORO_REENTER (coro) for (;;) - { - BOOST_ASIO_CORO_YIELD - asio::async_read_until( - conn->next_layer(), - conn->make_dynamic_buffer(), - "\r\n", std::move(self)); - - if (ec == 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->async_wait_receive(std::move(self)); - } else { - BOOST_ASSERT(conn->is_waiting_response()); - 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; - } - } - } -}; - -} // boost::redis::detail - -#endif // BOOST_REDIS_CONNECTION_OPS_HPP diff --git a/include/boost/redis/detail/connector.hpp b/include/boost/redis/detail/connector.hpp new file mode 100644 index 00000000..4e9c1507 --- /dev/null +++ b/include/boost/redis/detail/connector.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct connect_op { + Connector* ctor_ = nullptr; + Stream* stream = nullptr; + asio::ip::tcp::resolver::results_type const* res_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array 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 connector { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + connector(Executor ex) + : timer_{ex} + {} + + void set_config(config const& cfg) + { timeout_ = cfg.connect_timeout; } + + template + 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{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 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 diff --git a/include/boost/redis/detail/handshaker.hpp b/include/boost/redis/detail/handshaker.hpp new file mode 100644 index 00000000..6658aa9b --- /dev/null +++ b/include/boost/redis/detail/handshaker.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct handshake_op { + Handshaker* hsher_ = nullptr; + Stream* stream_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array 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 handshaker { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + handshaker(Executor ex) + : timer_{ex} + {} + + template + auto + async_handshake(Stream& stream, CompletionToken&& token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(handshake_op{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 friend struct handshake_op; + + timer_type timer_; + std::chrono::steady_clock::duration timeout_; +}; + +} // boost::redis::detail + +#endif // BOOST_REDIS_SSL_CONNECTOR_HPP diff --git a/include/boost/redis/detail/health_checker.hpp b/include/boost/redis/detail/health_checker.hpp new file mode 100644 index 00000000..dcf0292d --- /dev/null +++ b/include/boost/redis/detail/health_checker.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail { + +template +class ping_op { +public: + HealthChecker* checker_ = nullptr; + Connection* conn_ = nullptr; + asio::coroutine coro_{}; + + template + 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 check_timeout_op { +public: + HealthChecker* checker_ = nullptr; + Connection* conn_ = nullptr; + asio::coroutine coro_{}; + + template + 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 check_health_op { +public: + HealthChecker* checker_ = nullptr; + Connection* conn_ = nullptr; + asio::coroutine coro_{}; + + template + void + operator()( + Self& self, + std::array 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 health_checker { +private: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + 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 + > + 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{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 + auto async_ping(Connection& conn, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(ping_op{this, &conn}, token, conn, ping_timer_); + } + + template + auto async_check_timeout(Connection& conn, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(check_timeout_op{this, &conn}, token, conn, wait_timer_); + } + + template friend class ping_op; + template friend class check_timeout_op; + template 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 diff --git a/include/boost/redis/detail/helper.hpp b/include/boost/redis/detail/helper.hpp new file mode 100644 index 00000000..a8a21efe --- /dev/null +++ b/include/boost/redis/detail/helper.hpp @@ -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 + +namespace boost::redis::detail +{ + +template +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 diff --git a/include/boost/redis/detail/read.hpp b/include/boost/redis/detail/read.hpp index 0dfad6bc..9a74a498 100644 --- a/include/boost/redis/detail/read.hpp +++ b/include/boost/redis/detail/read.hpp @@ -9,14 +9,131 @@ #include #include -#include #include +#include #include #include -#include +#include +#include + +#include +#include namespace boost::redis::detail { +template +std::string_view buffer_view(DynamicBuffer buf) noexcept +{ + char const* start = static_cast(buf.data(0, buf.size()).data()); + return std::string_view{start, std::size(buf)}; +} + +template +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 + 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 +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 {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 + 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 @@ -58,43 +175,34 @@ read( ResponseAdapter adapter, system::error_code& ec) -> std::size_t { - resp3::parser p; - std::size_t n = 0; - std::size_t consumed = 0; - do { - if (!p.bulk_expected()) { - n = 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 = asio::read(stream, buf.data(s, to_read), ec); - if (ec) - return 0; - } - } + static std::size_t const growth = 1024; - auto const* data = static_cast(buf.data(0, n).data()); - auto const res = p.consume(data, n, ec); + resp3::parser parser; + while (!parser.done()) { + auto const res = parser.consume(detail::buffer_view(buf), ec); if (ec) - return 0; + return 0UL; - if (!p.bulk_expected()) { - adapter(res.first, ec); + 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 0; + return 0UL; + + buf.shrink(buf.size() - size_before - n); + continue; } - buf.consume(res.second); - consumed += res.second; - } while (!p.done()); + adapter(res.value(), ec); + if (ec) + return 0UL; + } - return consumed; + return parser.get_consumed(); } /** \brief Reads a complete response to a command sychronously. @@ -173,7 +281,7 @@ auto async_read( return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(detail::parse_op {stream, buffer, adapter}, + >(parse_op {stream, buffer, adapter}, token, stream); } diff --git a/include/boost/redis/detail/read_ops.hpp b/include/boost/redis/detail/read_ops.hpp deleted file mode 100644 index c99af843..00000000 --- a/include/boost/redis/detail/read_ops.hpp +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_READ_OPS_HPP -#define BOOST_REDIS_READ_OPS_HPP - -#include -#include -#include -#include -#include -#include - -#include - -namespace boost::redis::detail -{ -template -auto is_cancelled(T const& self) -{ - return self.get_cancellation_state().cancelled() != asio::cancellation_type_t::none; -} - -#define AEDIS_CHECK_OP0(X)\ - if (ec || redis::detail::is_cancelled(self)) {\ - X\ - self.complete(!!ec ? ec : asio::error::operation_aborted);\ - return;\ - } - -#define AEDIS_CHECK_OP1(X)\ - if (ec || redis::detail::is_cancelled(self)) {\ - X\ - self.complete(!!ec ? ec : asio::error::operation_aborted, {});\ - return;\ - } - -template < - class AsyncReadStream, - class DynamicBuffer, - class ResponseAdapter> -class parse_op { -private: - AsyncReadStream& stream_; - DynamicBuffer buf_; - resp3::parser parser_; - ResponseAdapter adapter_; - std::size_t consumed_ = 0; - std::size_t buffer_size_ = 0; - asio::coroutine coro_{}; - -public: - parse_op(AsyncReadStream& stream, DynamicBuffer buf, ResponseAdapter adapter) - : stream_ {stream} - , buf_ {std::move(buf)} - , adapter_ {std::move(adapter)} - { } - - template - void operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - BOOST_ASIO_CORO_REENTER (coro_) for (;;) { - if (!parser_.bulk_expected()) { - BOOST_ASIO_CORO_YIELD - 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 - asio::async_read( - stream_, - buf_.data(buffer_size_, parser_.bulk_length() + 2 - buffer_size_), - 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(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; - } - } - } -}; - -} // boost::redis::detail - -#endif // BOOST_REDIS_READ_OPS_HPP diff --git a/include/boost/redis/detail/resolver.hpp b/include/boost/redis/detail/resolver.hpp new file mode 100644 index 00000000..f4a31036 --- /dev/null +++ b/include/boost/redis/detail/resolver.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct resolve_op { + Resolver* resv_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array 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 resolver { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + resolver(Executor ex) : resv_{ex} , timer_{ex} {} + + template + auto async_resolve(CompletionToken&& token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(resolve_op{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; + template 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 diff --git a/include/boost/redis/detail/runner.hpp b/include/boost/redis/detail/runner.hpp new file mode 100644 index 00000000..e728b090 --- /dev/null +++ b/include/boost/redis/detail/runner.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct hello_op { + Runner* runner_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; + + template + 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_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 + void operator()( Self& self + , std::array 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 +struct run_all_op { + Runner* runner_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; + + template + 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 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 + auto async_run(Connection& conn, Logger l, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(runner_op{this, &conn, l}, token, conn); + } + + config const& get_config() const noexcept {return cfg_;} + +private: + using resolver_type = resolver; + using connector_type = connector; + using handshaker_type = detail::handshaker; + using health_checker_type = health_checker; + using timer_type = typename connector_type::timer_type; + + template friend struct run_all_op; + template friend class runner_op; + template friend struct hello_op; + + template + auto async_run_all(Connection& conn, Logger l, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(run_all_op{this, &conn, l}, token, conn); + } + + template + auto async_hello(Connection& conn, Logger l, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(hello_op{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 diff --git a/include/boost/redis/error.hpp b/include/boost/redis/error.hpp index b154dc56..7424aea7 100644 --- a/include/boost/redis/error.hpp +++ b/include/boost/redis/error.hpp @@ -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 diff --git a/include/boost/redis/experimental/run.hpp b/include/boost/redis/experimental/run.hpp deleted file mode 100644 index c71b47cb..00000000 --- a/include/boost/redis/experimental/run.hpp +++ /dev/null @@ -1,129 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_RUN_HPP -#define BOOST_REDIS_RUN_HPP - -// Has to included before promise.hpp to build on msvc. -#include -#include -#include -#include -#include - -namespace boost::redis::experimental { -namespace detail { - -template -class check_health_op { -private: - using executor_type = typename Connection::executor_type; - - struct state { - using clock_type = std::chrono::steady_clock; - using clock_traits_type = asio::wait_traits; - using timer_type = asio::basic_waitable_timer; - using promise_type = asio::experimental::promise; - - timer_type timer_; - request req_; - generic_response resp_; - std::optional prom_; - std::chrono::steady_clock::duration interval_; - - state( - executor_type ex, - std::string const& msg, - std::chrono::steady_clock::duration interval) - : timer_{ex} - , interval_{interval} - { - req_.push("PING", msg); - } - - void reset() - { - resp_.value().clear(); - prom_.reset(); - } - }; - - Connection* conn_ = nullptr; - std::shared_ptr state_ = nullptr; - asio::coroutine coro_{}; - -public: - check_health_op( - Connection& conn, - std::string const& msg, - std::chrono::steady_clock::duration interval) - : conn_{&conn} - , state_{std::make_shared(conn.get_executor(), msg, interval)} - { } - - template - void operator()(Self& self, system::error_code ec = {}, std::size_t = 0) - { - BOOST_ASIO_CORO_REENTER (coro_) for (;;) - { - state_->prom_.emplace(conn_->async_exec(state_->req_, state_->resp_, asio::experimental::use_promise)); - - state_->timer_.expires_after(state_->interval_); - BOOST_ASIO_CORO_YIELD - state_->timer_.async_wait(std::move(self)); - if (ec || is_cancelled(self) || state_->resp_.value().empty()) { - conn_->cancel(operation::run); - BOOST_ASIO_CORO_YIELD - std::move(*state_->prom_)(std::move(self)); - self.complete({}); - return; - } - - state_->reset(); - } - } -}; - -} // detail - -/** @brief Checks Redis health asynchronously - * @ingroup high-level-api - * - * This function will ping the Redis server periodically until a ping - * timesout or an error occurs. On timeout this function will - * complete with success. - * - * @param conn A connection to the Redis server. - * @param msg The message to be sent with the [PING](https://redis.io/commands/ping/) command. Seting a proper and unique id will help users identify which connections are active. - * @param interval Ping interval. - * @param token The completion token - * - * The completion token must have the following signature - * - * @code - * void f(system::error_code); - * @endcode - */ -template < - class Connection, - class CompletionToken = asio::default_completion_token_t -> -auto -async_check_health( - Connection& conn, - std::string const& msg = "Boost.Redis", - std::chrono::steady_clock::duration interval = std::chrono::seconds{2}, - CompletionToken token = CompletionToken{}) -{ - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(detail::check_health_op{conn, msg, interval}, token, conn); -} - -} // boost::redis::experimental - -#endif // BOOST_REDIS_RUN_HPP diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp new file mode 100644 index 00000000..9c83c145 --- /dev/null +++ b/include/boost/redis/impl/connection.ipp @@ -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 + +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 token) +{ + impl_.async_run(cfg, l, std::move(token)); +} + +void connection::cancel(operation op) +{ + impl_.cancel(op); +} + +} // namespace boost::redis diff --git a/include/boost/redis/impl/error.ipp b/include/boost/redis/impl/error.ipp index 42e72e76..9f5c06eb 100644 --- a/include/boost/redis/impl/error.ipp +++ b/include/boost/redis/impl/error.ipp @@ -38,6 +38,9 @@ struct error_category_impl : 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."; + 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."; } } diff --git a/include/boost/redis/impl/logger.ipp b/include/boost/redis/impl/logger.ipp new file mode 100644 index 00000000..d2c4db8c --- /dev/null +++ b/include/boost/redis/impl/logger.ipp @@ -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 +#include +#include +#include + +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 diff --git a/include/boost/redis/json.hpp b/include/boost/redis/json.hpp deleted file mode 100644 index 2d75c941..00000000 --- a/include/boost/redis/json.hpp +++ /dev/null @@ -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 BOOST_REDIS_JSON_HPP -#define BOOST_REDIS_JSON_HPP - -#include -#include - -namespace boost::redis::json -{ - -template -void boost_redis_to_bulk(std::string& to, T const& u) -{ - redis::resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u))); -} - -template -void boost_redis_from_bulk(T& u, std::string_view sv, system::error_code&) -{ - auto const jv = boost::json::parse(sv); - u = boost::json::value_to(jv); -} - -} // boost::redis::json - -using boost::redis::json::boost_redis_to_bulk; -using boost::redis::json::boost_redis_from_bulk; - -#endif // BOOST_REDIS_JSON_HPP diff --git a/include/boost/redis/logger.hpp b/include/boost/redis/logger.hpp new file mode 100644 index 00000000..e3a1cd35 --- /dev/null +++ b/include/boost/redis/logger.hpp @@ -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 +#include +#include + +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 diff --git a/include/boost/redis/operation.hpp b/include/boost/redis/operation.hpp index 99d4f2a9..d37145c7 100644 --- a/include/boost/redis/operation.hpp +++ b/include/boost/redis/operation.hpp @@ -9,19 +9,31 @@ namespace boost::redis { -/** \brief Connection operations that can be cancelled. - * \ingroup high-level-api +/** @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 diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 69f269f0..b3508c58 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -12,6 +12,7 @@ #include #include +#include // NOTE: For some commands like hset it would be a good idea to assert // the value type is a pair. @@ -98,6 +99,7 @@ class request { { payload_.clear(); commands_ = 0; + has_hello_priority_ = false; } /// Calls std::string::reserve on the internal storage. diff --git a/include/boost/redis/resp3/impl/parser.ipp b/include/boost/redis/resp3/impl/parser.ipp index cdaf3e65..89cae23c 100644 --- a/include/boost/redis/resp3/impl/parser.ipp +++ b/include/boost/redis/resp3/impl/parser.ipp @@ -24,132 +24,186 @@ parser::parser() sizes_[0] = 2; // The sentinel must be more than 1. } -auto -parser::consume( - char const* data, - std::size_t n, - system::error_code& ec) -> std::pair +std::size_t +parser::get_suggested_buffer_growth(std::size_t hint) const noexcept { - node_type ret; - if (bulk_expected()) { - n = bulk_length_ + 2; - ret = {bulk_, 1, depth_, {data, bulk_length_}}; - bulk_ = type::invalid; + 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_]; + } +} - } 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::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); - } +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. - if (data[1] != 'f' && data[1] != 't') { - ec = error::unexpected_bool_value; - return std::make_pair(node_type{}, 0); - } + 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 {}; - 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); - } + consumed_ = pos + 2; + if (!bulk_expected()) + return ret; - 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); + } [[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::max)(); + ret = {type::streamed_string, 0, depth_, {}}; + } else { + to_int(bulk_length_ , elem , ec); if (ec) - return std::make_pair(node_type{}, 0); + return {}; - 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); - } + 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 {}; + } - ++depth_; + 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 {}; - sizes_[depth_] = l * element_multiplicity(t); + ret = {t, l, depth_, {}}; + if (l == 0) { + commit_elem(); + } else { + if (depth_ == max_embedded_depth) { + ec = error::exceeeds_max_nested_depth; + return {}; } - } break; - default: - { - ec = error::invalid_data_type; - return std::make_pair(node_type{}, 0); + + ++depth_; + + sizes_[depth_] = l * element_multiplicity(t); } + } break; + default: + { + ec = error::invalid_data_type; + return {}; } } - - while (sizes_[depth_] == 0) { - --depth_; - --sizes_[depth_]; - } - - return std::make_pair(ret, n); + + return ret; } } // boost::redis::resp3 diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index b4efc6d2..3af8de4c 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -5,6 +5,7 @@ */ #include +#include namespace boost::redis::resp3 { @@ -14,29 +15,28 @@ void boost_redis_to_bulk(std::string& payload, std::string_view data) payload += to_code(type::blob_string); payload.append(std::cbegin(str), std::cend(str)); - payload += separator; + payload += parser::sep; payload.append(std::cbegin(data), std::cend(data)); - payload += separator; + payload += parser::sep; } void add_header(std::string& payload, type t, std::size_t size) { - // TODO: Call reserve. auto const str = std::to_string(size); payload += to_code(t); payload.append(std::cbegin(str), std::cend(str)); - payload += separator; + payload += parser::sep; } void add_blob(std::string& payload, std::string_view blob) { payload.append(std::cbegin(blob), std::cend(blob)); - payload += separator; + payload += parser::sep; } void add_separator(std::string& payload) { - payload += separator; + payload += parser::sep; } } // boost::redis::resp3 diff --git a/include/boost/redis/resp3/parser.hpp b/include/boost/redis/resp3/parser.hpp index 07517f3d..a66ebbae 100644 --- a/include/boost/redis/resp3/parser.hpp +++ b/include/boost/redis/resp3/parser.hpp @@ -8,21 +8,26 @@ #define BOOST_REDIS_RESP3_PARSER_HPP #include - +#include #include #include #include #include +#include namespace boost::redis::resp3 { using int_type = std::uint64_t; class parser { -private: +public: using node_type = basic_node; + using result = std::optional; + 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. @@ -40,30 +45,58 @@ class parser { // expected. type bulk_ = type::invalid; -public: - parser(); + // The number of bytes consumed from the buffer. + std::size_t consumed_ = 0; // 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; + auto consume_impl(type t, std::string_view elem, system::error_code& ec) -> node_type; - // Returns true when the parser is done with the current message. - [[nodiscard]] auto done() const noexcept - { return depth_ == 0 && bulk_ == type::invalid; } + 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 + [[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_; } +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 +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 diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index b1a8e38c..38ec138f 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -8,6 +8,9 @@ #define BOOST_REDIS_RESP3_SERIALIZATION_HPP #include +#include +#include +#include #include #include @@ -16,7 +19,6 @@ // to calculate the header size correctly. namespace boost::redis::resp3 { -constexpr char const* separator = "\r\n"; /** @brief Adds a bulk to the request. * @relates boost::redis::request @@ -84,7 +86,6 @@ void add_header(std::string& payload, type t, std::size_t size); template void add_bulk(std::string& payload, T const& data) { - // TODO: Call reserve. add_bulk_impl::add(payload, data); } @@ -104,6 +105,40 @@ struct bulk_counter> { void add_blob(std::string& payload, std::string_view blob); void add_separator(std::string& payload); +namespace detail +{ + +template +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 +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 diff --git a/include/boost/redis/resp3/type.hpp b/include/boost/redis/resp3/type.hpp index 4ea37432..32913750 100644 --- a/include/boost/redis/resp3/type.hpp +++ b/include/boost/redis/resp3/type.hpp @@ -7,6 +7,7 @@ #ifndef BOOST_REDIS_RESP3_TYPE_HPP #define BOOST_REDIS_RESP3_TYPE_HPP +#include #include #include #include diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index bec18b70..3a06c3e0 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -5,8 +5,10 @@ */ #include +#include #include #include +#include #include #include #include diff --git a/include/boost/redis/ssl/connection.hpp b/include/boost/redis/ssl/connection.hpp deleted file mode 100644 index 7b249d67..00000000 --- a/include/boost/redis/ssl/connection.hpp +++ /dev/null @@ -1,170 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_SSL_CONNECTION_HPP -#define BOOST_REDIS_SSL_CONNECTION_HPP - -#include -#include -#include - -#include -#include - -namespace boost::redis::ssl { - -template -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 Socket The socket type e.g. asio::ip::tcp::socket. - * - */ -template -class basic_connection> : - private redis::detail::connection_base< - typename asio::ssl::stream::executor_type, - basic_connection>> { -public: - /// Type of the next layer - using next_layer_type = asio::ssl::stream; - - /// Executor type. - using executor_type = typename next_layer_type::executor_type; - - /// Rebinds the socket type to another executor. - template - struct rebind_executor - { - /// The socket type when rebound to the specified executor. - using other = basic_connection::other>>; - }; - - using base_type = redis::detail::connection_base>>; - - /// Constructor - explicit - basic_connection(executor_type ex, asio::ssl::context& ctx) - : base_type{ex} - , stream_{ex, ctx} - { } - - /// Constructor - explicit - basic_connection(asio::io_context& ioc, asio::ssl::context& ctx) - : basic_connection(ioc.get_executor(), ctx) - { } - - /// Returns the associated executor. - auto get_executor() {return stream_.get_executor();} - - /// Reset the underlying stream. - void reset_stream(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 redis::connection::async_run for more information. - */ - template > - auto async_run(CompletionToken token = CompletionToken{}) - { - return base_type::async_run(std::move(token)); - } - - /** @brief Executes a command on the Redis server asynchronously. - * - * See redis::connection::async_exec for more information. - */ - template < - class Response = ignore_t, - class CompletionToken = asio::default_completion_token_t> - auto async_exec( - request const& req, - Response& response = ignore, - CompletionToken token = CompletionToken{}) - { - return base_type::async_exec(req, response, std::move(token)); - } - - /** @brief Receives server side pushes asynchronously. - * - * See redis::connection::async_receive for detailed information. - */ - template < - class Response = ignore_t, - class CompletionToken = asio::default_completion_token_t> - auto async_receive( - Response& response = ignore, - CompletionToken token = CompletionToken{}) - { - return base_type::async_receive(response, std::move(token)); - } - - /** @brief Cancel operations. - * - * See redis::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(); } - - /// Sets the maximum size of the read buffer. - void set_max_buffer_read_size(std::size_t max_read_size) noexcept - { base_type::set_max_buffer_read_size(max_read_size); } - - /** @brief Reserve memory on the read and write internal buffers. - * - * This function will call `std::string::reserve` on the - * underlying buffers. - * - * @param read The new capacity of the read buffer. - * @param write The new capacity of the write buffer. - */ - void reserve(std::size_t read, std::size_t write) - { base_type::reserve(read, write); } - -private: - using this_type = basic_connection; - - template friend class redis::detail::connection_base; - template friend struct redis::detail::exec_op; - template friend struct redis::detail::exec_read_op; - template friend struct detail::receive_op; - template friend struct redis::detail::run_op; - template friend struct redis::detail::writer_op; - template friend struct redis::detail::reader_op; - template friend struct detail::wait_receive_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. - * \ingroup high-level-api - */ -using connection = basic_connection>; - -} // boost::redis::ssl - -#endif // BOOST_REDIS_SSL_CONNECTION_HPP diff --git a/tests/common.cpp b/tests/common.cpp new file mode 100644 index 00000000..f4972ba2 --- /dev/null +++ b/tests/common.cpp @@ -0,0 +1,54 @@ +#include "common.hpp" +#include +#include +#include + +#include + +namespace net = boost::asio; + +struct run_callback { + std::shared_ptr 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 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 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 diff --git a/tests/common.hpp b/tests/common.hpp index a6eb4ba2..b5bc7dab 100644 --- a/tests/common.hpp +++ b/tests/common.hpp @@ -1,23 +1,25 @@ #pragma once -#include -#include - -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 +#include +#include +#include +#include +#include +#include #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 op) -> int; #endif // BOOST_ASIO_HAS_CO_AWAIT + +void +run( + std::shared_ptr 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); + diff --git a/tests/conn_check_health.cpp b/tests/conn_check_health.cpp deleted file mode 100644 index 5b7e2962..00000000 --- a/tests/conn_check_health.cpp +++ /dev/null @@ -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 -#include -#include - -#define BOOST_TEST_MODULE check-health -#include - -#include -#include -#include - -#include "common.hpp" - -namespace net = boost::asio; -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::experimental::async_check_health; - -std::chrono::seconds const interval{1}; - -struct push_callback { - connection* conn; - connection* conn2; - generic_response* resp; - request* req; - int i = 0; - - void operator()(error_code ec = {}, std::size_t = 0) - { - ++i; - if (ec) { - std::clog << "Exiting." << std::endl; - return; - } - - if (resp->value().empty()) { - // First call - BOOST_TEST(!ec); - conn2->async_receive(*resp, *this); - } else if (i == 5) { - std::clog << "Pausing the server" << std::endl; - // Pause the redis server to test if the health-check exits. - conn->async_exec(*req, ignore, [](auto ec, auto) { - std::clog << "Pausing callback> " << 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); - } else { - BOOST_TEST(!ec); - // Expect 3 pongs and pause the clients so check-health exists - // without error. - BOOST_TEST(resp->has_value()); - BOOST_TEST(!resp->value().empty()); - std::clog << "Event> " << resp->value().front().value << std::endl; - resp->value().clear(); - conn2->async_receive(*resp, *this); - } - }; -}; - -BOOST_AUTO_TEST_CASE(check_health) -{ - net::io_context ioc; - - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); - - // It looks like client pause does not work for clients that are - // sending MONITOR. I will therefore open a second connection. - connection conn2{ioc}; - net::connect(conn2.next_layer(), endpoints); - - std::string const msg = "test-check-health"; - - bool seen = false; - async_check_health(conn, msg, interval, [&](auto ec) { - BOOST_TEST(!ec); - std::cout << "async_check_health: completed." << std::endl; - seen = true; - }); - - request req; - req.push("HELLO", 3); - req.push("MONITOR"); - - conn2.async_exec(req, ignore, [](auto ec, auto) { - std::cout << "A" << std::endl; - BOOST_TEST(!ec); - }); - - request req2; - req2.push("HELLO", "3"); - req2.push("CLIENT", "PAUSE", "3000", "ALL"); - - generic_response resp; - push_callback{&conn, &conn2, &resp, &req2}(); // Starts reading pushes. - - conn.async_run([](auto ec){ - std::cout << "B" << std::endl; - BOOST_TEST(!!ec); - }); - - conn2.async_run([](auto ec){ - std::cout << "C" << std::endl; - BOOST_TEST(!!ec); - }); - - ioc.run(); - BOOST_TEST(seen); -} - diff --git a/tests/conn_echo_stress.cpp b/tests/conn_echo_stress.cpp deleted file mode 100644 index f6ef4156..00000000 --- a/tests/conn_echo_stress.cpp +++ /dev/null @@ -1,88 +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 -#include -#ifdef BOOST_ASIO_HAS_CO_AWAIT -#include -#define BOOST_TEST_MODULE echo-stress -#include -#include -#include -#include "common.hpp" -#include "../examples/common/common.hpp" - -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; - -auto push_consumer(std::shared_ptr conn, int expected) -> net::awaitable -{ - int c = 0; - for (;;) { - co_await conn->async_receive(ignore, net::use_awaitable); - if (++c == expected) - break; - } - - request req; - req.push("HELLO", 3); - req.push("QUIT"); - co_await conn->async_exec(req, ignore); -} - -auto echo_session(std::shared_ptr conn, std::string id, int n) -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - - request req; - response 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, resp, redir(ec)); - BOOST_CHECK_EQUAL(ec, boost::system::error_code{}); - BOOST_CHECK_EQUAL(msg, std::get<1>(resp).value()); - req.clear(); - std::get<1>(resp).value().clear(); - } -} - -auto async_echo_stress() -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(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 diff --git a/tests/conn_exec.cpp b/tests/conn_exec.cpp deleted file mode 100644 index 71ccf97c..00000000 --- a/tests/conn_exec.cpp +++ /dev/null @@ -1,125 +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 -#include -#include - -#define BOOST_TEST_MODULE conn-exec -#include - -#include -#include - -#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 error_code = boost::system::error_code; -using connection = boost::redis::connection; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore; -using boost::redis::ignore_t; - -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"); - req2.push("QUIT"); - - request req3; - 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, ignore, [&](auto ec, auto){ - std::cout << "bbb" << std::endl; - BOOST_TEST(!ec); - BOOST_TEST(!seen2); - BOOST_TEST(seen3); - seen1 = true; - }); - conn.async_exec(req2, ignore, [&](auto ec, auto){ - std::cout << "ccc" << std::endl; - BOOST_TEST(!ec); - BOOST_TEST(seen1); - BOOST_TEST(seen3); - seen2 = true; - }); - conn.async_exec(req3, ignore, [&](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) -{ - request req; - req.push("HELLO", 3); - req.push("QUIT"); - - // Wrong data type. - response resp; - net::io_context ioc; - - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); - - conn.async_exec(req, resp, [](auto ec, auto){ - BOOST_CHECK_EQUAL(ec, boost::redis::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) -{ - 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(ioc); - conn->async_exec(req, ignore, [](auto ec, auto){ - BOOST_CHECK_EQUAL(ec, boost::redis::error::not_connected); - }); - - ioc.run(); -} diff --git a/tests/conn_exec_cancel.cpp b/tests/conn_exec_cancel.cpp deleted file mode 100644 index 228d9659..00000000 --- a/tests/conn_exec_cancel.cpp +++ /dev/null @@ -1,175 +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 -#include -#ifdef BOOST_ASIO_HAS_CO_AWAIT -#include -#include -#define BOOST_TEST_MODULE conn-exec-cancel -#include -#include -#include -#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; -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; - -auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - - generic_response gresp; - auto conn = std::make_shared(ex); - co_await connect(conn, "127.0.0.1", "6379"); - - conn->async_run([conn](auto ec) { - std::cout << "async_run: " << ec.message() << std::endl; - BOOST_TEST(!ec); - }); - - net::steady_timer st{ex}; - st.expires_after(std::chrono::seconds{1}); - - // See NOTE1. - request req0; - req0.push("HELLO", 3); - co_await conn->async_exec(req0, gresp, net::use_awaitable); - - request req1; - req1.push("BLPOP", "any", 3); - - // Should not be canceled. - bool seen = false; - conn->async_exec(req1, gresp, [&](auto ec, auto) mutable{ - 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("QUIT"); - - // 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)); - - BOOST_TEST(!ec1); - BOOST_TEST(seen); -} - -auto ignore_implicit_cancel_of_req_written() -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(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. - request req0; - req0.push("HELLO", 3); - 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}); - - boost::system::error_code ec1, ec2; - co_await ( - conn->async_exec(req1, ignore, redir(ec1)) || - st.async_wait(redir(ec2)) - ); - - BOOST_CHECK_EQUAL(ec1, net::error::basic_errors::operation_aborted); - BOOST_TEST(!ec2); -} - -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) -{ - net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); - - request req0; - req0.push("HELLO", 3); - - // 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::basic_errors::operation_aborted); - }; - - auto c0 = [&](auto ec, auto) - { - BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); - }; - - conn.async_exec(req0, ignore, c0); - - conn.async_run([](auto ec){ - BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted); - }); - - net::steady_timer st{ioc}; - st.expires_after(std::chrono::seconds{1}); - st.async_wait([&](auto ec){ - BOOST_TEST(!ec); - conn.cancel(operation::run); - }); - - ioc.run(); -} - -#else -int main(){} -#endif diff --git a/tests/conn_tls.cpp b/tests/conn_tls.cpp deleted file mode 100644 index 5b9f2fe0..00000000 --- a/tests/conn_tls.cpp +++ /dev/null @@ -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 -#include -#include - -#define BOOST_TEST_MODULE conn-tls -#include - -#include -#include -#include -#include "common.hpp" - -namespace net = boost::asio; - -using connection = boost::redis::ssl::connection; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; - -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"); - - response resp; - - 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, resp, [](auto ec, auto) { - BOOST_TEST(!ec); - }); - - conn.async_run([](auto ec) { - BOOST_TEST(!ec); - }); - - ioc.run(); - - BOOST_CHECK_EQUAL(in, std::get<1>(resp).value()); -} - diff --git a/tests/issue_50.cpp b/tests/issue_50.cpp deleted file mode 100644 index b2675a3a..00000000 --- a/tests/issue_50.cpp +++ /dev/null @@ -1,78 +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) - */ - -// Must come before any asio header, otherwise build fails on msvc. -#include - -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include -#include -#include - -#include "../examples/common/common.hpp" - -namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using steady_timer = net::use_awaitable_t<>::as_default_on_t; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore; -using boost::redis::experimental::async_check_health; - -// Push consumer -auto receiver(std::shared_ptr conn) -> net::awaitable -{ - for (;;) - co_await conn->async_receive(); -} - -auto periodic_task(std::shared_ptr conn) -> net::awaitable -{ - net::steady_timer timer{co_await net::this_coro::executor}; - for (int i = 0; i < 10; ++i) { - timer.expires_after(std::chrono::seconds(2)); - co_await timer.async_wait(net::use_awaitable); - - // Key is not set so it will cause an error since we are passing - // an adapter that does not accept null, this will cause an error - // that result in the connection being closed. - request req; - req.push("GET", "mykey"); - auto [ec, u] = co_await conn->async_exec(req, ignore, net::as_tuple(net::use_awaitable)); - if (ec) { - std::cout << "Error: " << ec << std::endl; - } else { - std::cout << "no error: " << std::endl; - } - } - - std::cout << "Periodic task done!" << std::endl; -} - -auto co_main(std::string host, std::string port) -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - steady_timer timer{ex}; - - request req; - req.push("HELLO", 3); - req.push("SUBSCRIBE", "channel"); - - // The loop will reconnect on connection lost. To exit type Ctrl-C twice. - for (int i = 0; i < 10; ++i) { - co_await connect(conn, host, port); - co_await ((conn->async_run() || receiver(conn) || async_check_health(*conn) || periodic_task(conn)) && - conn->async_exec(req)); - - conn->reset_stream(); - timer.expires_after(std::chrono::seconds{1}); - co_await timer.async_wait(); - } -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/tests/test_conn_check_health.cpp b/tests/test_conn_check_health.cpp new file mode 100644 index 00000000..dc8fa078 --- /dev/null +++ b/tests/test_conn_check_health.cpp @@ -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 +#include +#define BOOST_TEST_MODULE check-health +#include +#include +#include +#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}); +} + diff --git a/tests/test_conn_echo_stress.cpp b/tests/test_conn_echo_stress.cpp new file mode 100644 index 00000000..a967a32f --- /dev/null +++ b/tests/test_conn_echo_stress.cpp @@ -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 +#include +#include +#include +#include +#define BOOST_TEST_MODULE echo-stress +#include +#include +#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 conn, int expected) -> net::awaitable +{ + 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 conn, + std::shared_ptr pubs, + std::string id, + int n) -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + + request req; + response 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 +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(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(); + 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 diff --git a/tests/test_conn_exec.cpp b/tests/test_conn_exec.cpp new file mode 100644 index 00000000..bd5bc5ed --- /dev/null +++ b/tests/test_conn_exec.cpp @@ -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 +#include +#define BOOST_TEST_MODULE conn-exec +#include +#include +#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(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 resp; + net::io_context ioc; + + auto conn = std::make_shared(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(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(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); +} + diff --git a/tests/test_conn_exec_cancel.cpp b/tests/test_conn_exec_cancel.cpp new file mode 100644 index 00000000..ccf3f4b2 --- /dev/null +++ b/tests/test_conn_exec_cancel.cpp @@ -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 +#include +#define BOOST_TEST_MODULE conn-exec-cancel +#include +#include +#include "common.hpp" +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include + +// 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 +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(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(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 diff --git a/tests/test_conn_exec_cancel2.cpp b/tests/test_conn_exec_cancel2.cpp new file mode 100644 index 00000000..f7ed3395 --- /dev/null +++ b/tests/test_conn_exec_cancel2.cpp @@ -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 +#include +#define BOOST_TEST_MODULE conn-exec-cancel +#include +#include +#include "common.hpp" +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include + +// 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 +{ + auto ex = co_await net::this_coro::executor; + + generic_response gresp; + auto conn = std::make_shared(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 diff --git a/tests/conn_exec_error.cpp b/tests/test_conn_exec_error.cpp similarity index 72% rename from tests/conn_exec_error.cpp rename to tests/test_conn_exec_error.cpp index 000b54ac..fe465325 100644 --- a/tests/conn_exec_error.cpp +++ b/tests/test_conn_exec_error.cpp @@ -4,17 +4,13 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include - #define BOOST_TEST_MODULE conn-exec-error #include - -#include -#include - #include "common.hpp" +#include namespace net = boost::asio; namespace redis = boost::redis; @@ -27,6 +23,10 @@ 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) { @@ -37,18 +37,15 @@ BOOST_AUTO_TEST_CASE(no_ignore_error) net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); - conn.async_exec(req, ignore, [&](auto ec, auto){ + conn->async_exec(req, ignore, [&](auto ec, auto){ BOOST_CHECK_EQUAL(ec, error::resp3_simple_error); - conn.cancel(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); - conn.async_run([](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + run(conn); ioc.run(); } @@ -66,12 +63,10 @@ BOOST_AUTO_TEST_CASE(has_diagnostic) net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); response resp; - conn.async_exec(req, resp, [&](auto ec, auto){ + conn->async_exec(req, resp, [&](auto ec, auto){ BOOST_TEST(!ec); // HELLO @@ -85,12 +80,11 @@ BOOST_AUTO_TEST_CASE(has_diagnostic) BOOST_TEST(std::get<1>(resp).has_value()); BOOST_CHECK_EQUAL(std::get<1>(resp).value(), "Barra do Una"); - conn.cancel(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); - conn.async_run([](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + run(conn); ioc.run(); } @@ -111,16 +105,15 @@ BOOST_AUTO_TEST_CASE(resp3_error_in_cmd_pipeline) response resp2; net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(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(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }; auto c1 = [&](auto ec, auto) @@ -135,14 +128,11 @@ BOOST_AUTO_TEST_CASE(resp3_error_in_cmd_pipeline) 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(req2, resp2, c2); }; - conn.async_exec(req1, resp1, c1); - - conn.async_run([](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + conn->async_exec(req1, resp1, c1); + run(conn); ioc.run(); } @@ -171,11 +161,9 @@ BOOST_AUTO_TEST_CASE(error_in_transaction) net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); - conn.async_exec(req, resp, [&](auto ec, auto){ + conn->async_exec(req, resp, [&](auto ec, auto){ BOOST_TEST(!ec); BOOST_TEST(std::get<0>(resp).has_value()); @@ -203,32 +191,40 @@ BOOST_AUTO_TEST_CASE(error_in_transaction) BOOST_TEST(std::get<6>(resp).has_value()); BOOST_CHECK_EQUAL(std::get<6>(resp).value(), "PONG"); - conn.cancel(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); - conn.async_run([](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + run(conn); ioc.run(); } -// This test is important because a subscriber has no response on -// success, but on error, for example when using a wrong syntax, the -// server will send a simple error response the client is not -// expecting. +// 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("HELLO", 3); + req1.push("PING"); request req2; req2.push("SUBSCRIBE"); // Wrong command synthax. net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); auto c2 = [&](auto ec, auto) { @@ -240,10 +236,10 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax) { std::cout << "async_exec: hello" << std::endl; BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); generic_response gresp; auto c3 = [&](auto ec, auto) @@ -254,15 +250,13 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax) 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(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }; - conn.async_receive(gresp, c3); + conn->async_receive(gresp, c3); - conn.async_run([](auto ec){ - std::cout << "async_run" << std::endl; - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + run(conn); ioc.run(); } diff --git a/tests/conn_exec_retry.cpp b/tests/test_conn_exec_retry.cpp similarity index 70% rename from tests/conn_exec_retry.cpp rename to tests/test_conn_exec_retry.cpp index c47808a0..7cc1f232 100644 --- a/tests/conn_exec_retry.cpp +++ b/tests/test_conn_exec_retry.cpp @@ -4,16 +4,13 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include #define BOOST_TEST_MODULE conn-exec-retry #include - -#include -#include - +#include #include "common.hpp" namespace net = boost::asio; @@ -23,6 +20,9 @@ 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) { @@ -40,7 +40,7 @@ BOOST_AUTO_TEST_CASE(request_retry_false) req2.push("PING"); net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); net::steady_timer st{ioc}; st.expires_after(std::chrono::seconds{1}); @@ -50,31 +50,33 @@ BOOST_AUTO_TEST_CASE(request_retry_false) // although it has cancel_on_connection_lost = false. The reason // being it has already been written so // cancel_on_connection_lost does not apply. - conn.cancel(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); }; 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, ignore, c1); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - 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(); } @@ -100,28 +102,27 @@ BOOST_AUTO_TEST_CASE(request_retry_true) req3.push("QUIT"); net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(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(boost::redis::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); - auto c3 = [&](auto ec, auto){ + std::cout << "c3: " << ec.message() << std::endl; BOOST_TEST(!ec); + conn->cancel(); }; auto c2 = [&](auto ec, auto){ BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c3); + conn->async_exec(req3, ignore, c3); }; auto c1 = [](auto ec, auto){ @@ -130,23 +131,17 @@ BOOST_AUTO_TEST_CASE(request_retry_true) auto c0 = [&](auto ec, auto){ BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req1, ignore, c1); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - conn.async_run([&](auto ec){ - // The first cacellation. - 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); - }); + config cfg; + cfg.health_check_interval = 5s; + conn->async_run(cfg, {}, [&](auto ec){ + std::cout << ec.message() << std::endl; + BOOST_TEST(!!ec); }); ioc.run(); diff --git a/tests/conn_push.cpp b/tests/test_conn_push.cpp similarity index 60% rename from tests/conn_push.cpp rename to tests/test_conn_push.cpp index a097f2cb..91d1f274 100644 --- a/tests/conn_push.cpp +++ b/tests/test_conn_push.cpp @@ -4,21 +4,19 @@ * accompanying file LICENSE.txt) */ -#include -#include -#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include +#include #include +#include +#include #include - #define BOOST_TEST_MODULE conn-push #include - -#include -#include +#include #include "common.hpp" namespace net = boost::asio; -namespace resp3 = boost::redis::resp3; +namespace redis = boost::redis; using boost::redis::operation; using connection = boost::redis::connection; @@ -28,13 +26,96 @@ using boost::redis::request; using boost::redis::response; using boost::redis::ignore; using boost::redis::ignore_t; +using redis::config; +using boost::redis::logger; +using namespace std::chrono_literals; + +BOOST_AUTO_TEST_CASE(receives_push_waiting_resps) +{ + request req1; + req1.push("HELLO", 3); + req1.push("PING", "Message1"); + + request req2; + req2.push("SUBSCRIBE", "channel"); + + request req3; + req3.push("PING", "Message2"); + req3.push("QUIT"); + + net::io_context ioc; + + auto conn = std::make_shared(ioc); + + auto c3 =[](auto ec, auto...) + { + BOOST_TEST(!!ec); + }; + + auto c2 =[&, conn](auto ec, auto...) + { + BOOST_TEST(!ec); + conn->async_exec(req3, ignore, c3); + }; + + auto c1 =[&, conn](auto ec, auto...) + { + BOOST_TEST(!ec); + conn->async_exec(req2, ignore, c2); + }; + + conn->async_exec(req1, ignore, c1); + + run(conn, {}, {}); + + bool push_received = false; + conn->async_receive(ignore, [&, conn](auto ec, auto){ + std::cout << "async_receive" << std::endl; + BOOST_TEST(!ec); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); + push_received = true; + }); + + ioc.run(); + + BOOST_TEST(push_received); +} + +BOOST_AUTO_TEST_CASE(push_received1) +{ + net::io_context ioc; + auto conn = std::make_shared(ioc); + + request req; + //req.push("HELLO", 3); + req.push("SUBSCRIBE", "channel"); + + conn->async_exec(req, ignore, [conn](auto ec, auto){ + std::cout << "async_exec" << std::endl; + BOOST_TEST(!ec); + }); + + run(conn); + + bool push_received = false; + conn->async_receive(ignore, [&, conn](auto ec, auto){ + std::cout << "async_receive" << std::endl; + BOOST_TEST(!ec); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); + push_received = true; + }); + + ioc.run(); + + BOOST_TEST(push_received); +} BOOST_AUTO_TEST_CASE(push_filtered_out) { net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); request req; req.push("HELLO", 3); @@ -43,17 +124,16 @@ BOOST_AUTO_TEST_CASE(push_filtered_out) req.push("QUIT"); response resp; - conn.async_exec(req, resp, [](auto ec, auto){ + conn->async_exec(req, resp, [conn](auto ec, auto){ BOOST_TEST(!ec); }); - conn.async_receive(ignore, [](auto ec, auto){ + conn->async_receive(ignore, [conn](auto ec, auto){ BOOST_TEST(!ec); + conn->cancel(operation::reconnection); }); - conn.async_run([](auto ec){ - BOOST_TEST(!ec); - }); + run(conn); ioc.run(); @@ -62,16 +142,17 @@ BOOST_AUTO_TEST_CASE(push_filtered_out) } #ifdef BOOST_ASIO_HAS_CO_AWAIT -net::awaitable push_consumer1(connection& conn, bool& push_received) +net::awaitable +push_consumer1(std::shared_ptr conn, bool& push_received) { { - auto [ec, ev] = co_await conn.async_receive(ignore, as_tuple(net::use_awaitable)); + auto [ec, ev] = co_await conn->async_receive(ignore, as_tuple(net::use_awaitable)); BOOST_TEST(!ec); } { - auto [ec, ev] = co_await conn.async_receive(ignore, as_tuple(net::use_awaitable)); - BOOST_CHECK_EQUAL(ec, net::experimental::channel_errc::channel_cancelled); + auto [ec, ev] = co_await conn->async_receive(ignore, as_tuple(net::use_awaitable)); + BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); } push_received = true; @@ -101,9 +182,7 @@ auto boost_redis_adapt(response_error_tag&) BOOST_AUTO_TEST_CASE(test_push_adapter) { net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); request req; req.push("HELLO", 3); @@ -111,114 +190,28 @@ BOOST_AUTO_TEST_CASE(test_push_adapter) req.push("SUBSCRIBE", "channel"); req.push("PING"); - conn.async_receive(error_tag_obj, [](auto ec, auto) { + conn->async_receive(error_tag_obj, [conn](auto ec, auto) { BOOST_CHECK_EQUAL(ec, boost::redis::error::incompatible_size); + conn->cancel(operation::reconnection); }); - conn.async_exec(req, ignore, [](auto ec, auto){ - BOOST_CHECK_EQUAL(ec, net::experimental::error::channel_errors::channel_cancelled); - }); - - conn.async_run([](auto ec){ + conn->async_exec(req, ignore, [](auto ec, auto){ BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); }); + run(conn); + ioc.run(); // TODO: Reset the ioc reconnect and send a quit to ensure // reconnection is possible after an error. } -net::awaitable push_consumer3(connection& conn) +net::awaitable push_consumer3(std::shared_ptr conn) { - for (;;) - co_await conn.async_receive(ignore, net::use_awaitable); -} - -BOOST_AUTO_TEST_CASE(push_received1) -{ - 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("SUBSCRIBE", "channel"); - req.push("QUIT"); - - conn.async_exec(req, ignore, [](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); -} - -BOOST_AUTO_TEST_CASE(receives_push_waiting_resps) -{ - request req1; - req1.push("HELLO", 3); - req1.push("PING", "Message1"); - - request req2; - req2.push("SUBSCRIBE", "channel"); - - request req3; - 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 c3 =[](auto ec, auto...) - { - BOOST_TEST(!ec); - }; - - auto c2 =[&](auto ec, auto...) - { - BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c3); - }; - - auto c1 =[&](auto ec, auto...) - { - BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); - }; - - conn.async_exec(req1, ignore, c1); - - 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); + for (;;) { + co_await conn->async_receive(ignore, net::use_awaitable); + } } BOOST_AUTO_TEST_CASE(many_subscribers) @@ -240,82 +233,75 @@ BOOST_AUTO_TEST_CASE(many_subscribers) req3.push("QUIT"); net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); auto c11 =[&](auto ec, auto...) { + std::cout << "quit sent" << std::endl; + conn->cancel(operation::reconnection); BOOST_TEST(!ec); }; auto c10 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c11); + conn->async_exec(req3, ignore, c11); }; auto c9 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c10); + conn->async_exec(req2, ignore, c10); }; auto c8 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c9); + conn->async_exec(req1, ignore, c9); }; auto c7 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c8); + conn->async_exec(req2, ignore, c8); }; auto c6 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c7); + conn->async_exec(req2, ignore, c7); }; auto c5 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c6); + conn->async_exec(req1, ignore, c6); }; auto c4 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c5); + conn->async_exec(req2, ignore, c5); }; auto c3 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c4); + conn->async_exec(req1, ignore, c4); }; auto c2 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c3); + conn->async_exec(req2, ignore, c3); }; auto c1 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; auto c0 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - conn.async_run([&](auto ec) { - BOOST_TEST(!ec); - conn.cancel(operation::receive); - }); + run(conn, {}, {}); net::co_spawn(ioc.get_executor(), push_consumer3(conn), net::detached); ioc.run(); } #endif - -#else -int main() {} -#endif diff --git a/tests/conn_quit.cpp b/tests/test_conn_quit.cpp similarity index 53% rename from tests/conn_quit.cpp rename to tests/test_conn_quit.cpp index 4c1d2c2b..bf126ba1 100644 --- a/tests/conn_quit.cpp +++ b/tests/test_conn_quit.cpp @@ -4,57 +4,47 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #include - #define BOOST_TEST_MODULE conn-quit #include - -#include -#include +#include #include "common.hpp" namespace net = boost::asio; - -using connection = boost::redis::connection; -using error_code = boost::system::error_code; -using operation = boost::redis::operation; +using boost::redis::connection; +using boost::system::error_code; +using boost::redis::operation; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; +using boost::redis::config; +using namespace std::chrono_literals; -BOOST_AUTO_TEST_CASE(test_quit1) +BOOST_AUTO_TEST_CASE(test_eof_no_error) { request req; req.get_config().cancel_on_connection_lost = false; - req.push("HELLO", 3); req.push("QUIT"); net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); - conn.async_exec(req, ignore, [](auto ec, auto) { - BOOST_TEST(!ec); - }); - - conn.async_run([](auto ec) { + conn->async_exec(req, ignore, [&](auto ec, auto) { BOOST_TEST(!ec); + conn->cancel(operation::reconnection); }); + run(conn); ioc.run(); } // Test if quit causes async_run to exit. -BOOST_AUTO_TEST_CASE(test_quit2) +BOOST_AUTO_TEST_CASE(test_async_run_exits) { net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); + auto conn = std::make_shared(ioc); request req1; req1.get_config().cancel_on_connection_lost = false; @@ -64,38 +54,39 @@ BOOST_AUTO_TEST_CASE(test_quit2) req2.get_config().cancel_on_connection_lost = false; req2.push("QUIT"); + // Should fail since this request will be sent after quit. request req3; - // Should cause the request to fail since this request will be sent - // after quit. req3.get_config().cancel_if_not_connected = true; req3.push("PING"); auto c3 = [](auto ec, auto) { - std::cout << "3--> " << ec.message() << std::endl; + std::clog << "c3: " << ec.message() << std::endl; BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); }; auto c2 = [&](auto ec, auto) { - std::cout << "2--> " << ec.message() << std::endl; + std::clog << "c2: " << ec.message() << std::endl; BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c3); + conn->async_exec(req3, ignore, c3); }; auto c1 = [&](auto ec, auto) { - std::cout << "1--> " << ec.message() << std::endl; + std::cout << "c1: " << ec.message() << std::endl; BOOST_TEST(!ec); - - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); - conn.async_run([&](auto ec){ - BOOST_TEST(!ec); - }); + // The healthy checker should not be the cause of async_run + // completing, so we disable. + config cfg; + cfg.health_check_interval = 0s; + cfg.reconnect_wait_interval = 0s; + run(conn, cfg); ioc.run(); } diff --git a/tests/conn_reconnect.cpp b/tests/test_conn_reconnect.cpp similarity index 66% rename from tests/conn_reconnect.cpp rename to tests/test_conn_reconnect.cpp index f07264f0..761f041d 100644 --- a/tests/conn_reconnect.cpp +++ b/tests/test_conn_reconnect.cpp @@ -4,25 +4,27 @@ * accompanying file LICENSE.txt) */ -#include -#include -#ifdef BOOST_ASIO_HAS_CO_AWAIT - +#include +#include #define BOOST_TEST_MODULE conn-reconnect #include - -#include -#include +#include #include "common.hpp" -#include "../examples/common/common.hpp" + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include namespace net = boost::asio; -using error_code = boost::system::error_code; +using boost::system::error_code; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; +using boost::redis::config; +using boost::redis::logger; +using boost::redis::operation; +using boost::redis::connection; +using namespace std::chrono_literals; -#include using namespace boost::asio::experimental::awaitable_operators; net::awaitable test_reconnect_impl() @@ -32,23 +34,20 @@ net::awaitable test_reconnect_impl() request req; req.push("QUIT"); - auto const endpoints = resolve(); - connection conn{ex}; + auto conn = std::make_shared(ex); + run(conn); int i = 0; for (; i < 5; ++i) { - boost::system::error_code ec1, ec2; - net::connect(conn.next_layer(), endpoints); - co_await ( - conn.async_exec(req, ignore, net::redirect_error(net::use_awaitable, ec1)) && - conn.async_run(net::redirect_error(net::use_awaitable, ec2)) - ); - - BOOST_TEST(!ec1); - BOOST_TEST(!ec2); - conn.reset_stream(); + error_code ec1, ec2; + config cfg; + logger l; + co_await conn->async_exec(req, ignore, net::redirect_error(net::use_awaitable, ec1)); + //BOOST_TEST(!ec); + std::cout << "test_reconnect: " << i << " " << ec2.message() << " " << ec1.message() << std::endl; } + conn->cancel(); BOOST_CHECK_EQUAL(i, 5); co_return; } @@ -68,51 +67,51 @@ auto async_test_reconnect_timeout() -> net::awaitable net::steady_timer st{ex}; auto conn = std::make_shared(ex); - boost::system::error_code ec1, ec2, ec3; + error_code ec1, ec3; request req1; req1.get_config().cancel_if_not_connected = false; req1.get_config().cancel_on_connection_lost = true; req1.get_config().cancel_if_unresponded = true; - req1.push("HELLO", 3); req1.push("BLPOP", "any", 0); - co_await connect(conn, "127.0.0.1", "6379"); st.expires_after(std::chrono::seconds{1}); + config cfg; co_await ( conn->async_exec(req1, ignore, redir(ec1)) || - conn->async_run(redir(ec2)) || st.async_wait(redir(ec3)) ); //BOOST_TEST(!ec1); - BOOST_CHECK_EQUAL(ec2, boost::system::errc::errc_t::operation_canceled); //BOOST_TEST(!ec3); request req2; req2.get_config().cancel_if_not_connected = false; req2.get_config().cancel_on_connection_lost = true; req2.get_config().cancel_if_unresponded= true; - req2.push("HELLO", 3); req2.push("QUIT"); - co_await connect(conn, "127.0.0.1", "6379"); st.expires_after(std::chrono::seconds{1}); co_await ( conn->async_exec(req1, ignore, net::redirect_error(net::use_awaitable, ec1)) || - conn->async_run(net::redirect_error(net::use_awaitable, ec2)) || st.async_wait(net::redirect_error(net::use_awaitable, ec3)) ); + conn->cancel(); + std::cout << "ccc" << std::endl; BOOST_CHECK_EQUAL(ec1, boost::system::errc::errc_t::operation_canceled); - BOOST_CHECK_EQUAL(ec2, boost::asio::error::basic_errors::operation_aborted); } BOOST_AUTO_TEST_CASE(test_reconnect_and_idle) { - run(async_test_reconnect_timeout()); + net::io_context ioc; + net::co_spawn(ioc, async_test_reconnect_timeout(), net::detached); + ioc.run(); } #else -int main(){} +BOOST_AUTO_TEST_CASE(dummy) +{ + BOOST_TEST(true); +} #endif diff --git a/tests/conn_run_cancel.cpp b/tests/test_conn_run_cancel.cpp similarity index 72% rename from tests/conn_run_cancel.cpp rename to tests/test_conn_run_cancel.cpp index 1218e56f..09934c2b 100644 --- a/tests/conn_run_cancel.cpp +++ b/tests/test_conn_run_cancel.cpp @@ -4,46 +4,48 @@ * accompanying file LICENSE.txt) */ -#include -#include -#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include +#include +#include #include -#include - #define BOOST_TEST_MODULE conn-run-cancel #include - -#include -#include +#include #include "common.hpp" +#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include +#include + namespace net = boost::asio; using boost::redis::operation; -using connection = boost::redis::connection; -using error_code = boost::system::error_code; +using boost::redis::config; +using boost::redis::connection; +using boost::system::error_code; using net::experimental::as_tuple; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; +using boost::redis::logger; +using namespace std::chrono_literals; -#include using namespace net::experimental::awaitable_operators; auto async_cancel_run_with_timer() -> net::awaitable { auto ex = co_await net::this_coro::executor; - auto const endpoints = resolve(); connection conn{ex}; - net::connect(conn.next_layer(), endpoints); net::steady_timer st{ex}; - st.expires_after(std::chrono::seconds{1}); + st.expires_after(1s); - boost::system::error_code ec1, ec2; - co_await (conn.async_run(redir(ec1)) || st.async_wait(redir(ec2))); + error_code ec1, ec2; + config cfg; + logger l; + co_await (conn.async_run(cfg, l, redir(ec1)) || st.async_wait(redir(ec2))); - BOOST_CHECK_EQUAL(ec1, boost::asio::error::basic_errors::operation_aborted); + BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); BOOST_TEST(!ec2); } @@ -58,17 +60,17 @@ auto async_check_cancellation_not_missed(int n, std::chrono::milliseconds ms) -> net::awaitable { auto ex = co_await net::this_coro::executor; - auto const endpoints = resolve(); connection conn{ex}; net::steady_timer timer{ex}; for (auto i = 0; i < n; ++i) { timer.expires_after(ms); - net::connect(conn.next_layer(), endpoints); - boost::system::error_code ec1, ec2; - co_await (conn.async_run(redir(ec1)) || timer.async_wait(redir(ec2))); - BOOST_CHECK_EQUAL(ec1, boost::asio::error::basic_errors::operation_aborted); + error_code ec1, ec2; + config cfg; + logger l; + co_await (conn.async_run(cfg, l, redir(ec1)) || timer.async_wait(redir(ec2))); + BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); std::cout << "Counter: " << i << std::endl; } } @@ -144,31 +146,9 @@ BOOST_AUTO_TEST_CASE(check_implicit_cancel_not_missed_1024) ioc.run(); } -BOOST_AUTO_TEST_CASE(reset_before_run_completes) +#else +BOOST_AUTO_TEST_CASE(dummy) { - net::io_context ioc; - auto const endpoints = resolve(); - connection conn{ioc}; - net::connect(conn.next_layer(), endpoints); - - - // Sends a ping just as a means of waiting until we are connected. - request req; - req.push("HELLO", 3); - req.push("PING"); - - conn.async_exec(req, ignore, [&](auto ec, auto){ - BOOST_TEST(!ec); - conn.reset_stream(); - }); - - conn.async_run([&](auto ec){ - BOOST_CHECK_EQUAL(ec, net::error::operation_aborted); - }); - - ioc.run(); + BOOST_TEST(true); } - -#else -int main(){} #endif diff --git a/tests/test_conn_tls.cpp b/tests/test_conn_tls.cpp new file mode 100644 index 00000000..fef978ff --- /dev/null +++ b/tests/test_conn_tls.cpp @@ -0,0 +1,60 @@ +/* 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 +#define BOOST_TEST_MODULE conn-tls +#include +#include +#include "common.hpp" + +namespace net = boost::asio; + +using connection = boost::redis::connection; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::operation; + +bool verify_certificate(bool, net::ssl::verify_context&) +{ + std::cout << "set_verify_callback" << std::endl; + return true; +} + +BOOST_AUTO_TEST_CASE(ping) +{ + config cfg; + cfg.use_ssl = true; + cfg.username = "aedis"; + cfg.password = "aedis"; + cfg.addr.host = "db.occase.de"; + cfg.addr.port = "6380"; + + std::string const in = "Kabuf"; + + request req; + req.push("PING", in); + + response resp; + + net::io_context ioc; + connection conn{ioc}; + conn.next_layer().set_verify_mode(net::ssl::verify_peer); + conn.next_layer().set_verify_callback(verify_certificate); + + conn.async_exec(req, resp, [&](auto ec, auto) { + BOOST_TEST(!ec); + conn.cancel(); + }); + + conn.async_run(cfg, {}, [](auto) { }); + + ioc.run(); + + BOOST_CHECK_EQUAL(in, std::get<0>(resp).value()); + std::cout << "===============================" << std::endl; +} + diff --git a/tests/test_issue_50.cpp b/tests/test_issue_50.cpp new file mode 100644 index 00000000..dabffd84 --- /dev/null +++ b/tests/test_issue_50.cpp @@ -0,0 +1,108 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +// Must come before any asio header, otherwise build fails on msvc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define BOOST_TEST_MODULE conn-quit +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace net = boost::asio; +using steady_timer = net::use_awaitable_t<>::as_default_on_t; +using boost::redis::request; +using boost::redis::response; +using boost::redis::ignore; +using boost::redis::logger; +using boost::redis::config; +using boost::redis::operation; +using boost::redis::connection; +using boost::system::error_code; +using boost::asio::use_awaitable; +using boost::asio::redirect_error; +using namespace std::chrono_literals; + +// Push consumer +auto +receiver(std::shared_ptr conn) -> net::awaitable +{ + std::cout << "uuu" << std::endl; + while (conn->will_reconnect()) { + std::cout << "dddd" << std::endl; + // Loop reading Redis pushs messages. + for (;;) { + std::cout << "aaaa" << std::endl; + error_code ec; + co_await conn->async_receive(ignore, redirect_error(use_awaitable, ec)); + if (ec) + break; + } + } +} + +auto +periodic_task(std::shared_ptr conn) -> net::awaitable +{ + net::steady_timer timer{co_await net::this_coro::executor}; + for (int i = 0; i < 10; ++i) { + std::cout << "In the loop: " << i << std::endl; + timer.expires_after(std::chrono::milliseconds(50)); + co_await timer.async_wait(net::use_awaitable); + + // Key is not set so it will cause an error since we are passing + // an adapter that does not accept null, this will cause an error + // that result in the connection being closed. + request req; + req.push("GET", "mykey"); + auto [ec, u] = co_await conn->async_exec(req, ignore, net::as_tuple(net::use_awaitable)); + if (ec) { + std::cout << "(1)Error: " << ec << std::endl; + } else { + std::cout << "no error: " << std::endl; + } + } + + std::cout << "Periodic task done!" << std::endl; + conn->cancel(operation::run); + conn->cancel(operation::receive); + conn->cancel(operation::reconnection); +} + +auto co_main(config cfg) -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(ex); + + net::co_spawn(ex, receiver(conn), net::detached); + net::co_spawn(ex, periodic_task(conn), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); +} + +BOOST_AUTO_TEST_CASE(issue_50) +{ + net::io_context ioc; + net::co_spawn(ioc, std::move(co_main({})), net::detached); + ioc.run(); +} + +#else // defined(BOOST_ASIO_HAS_CO_AWAIT) + +BOOST_AUTO_TEST_CASE(issue_50) +{ +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/tests/low_level.cpp b/tests/test_low_level.cpp similarity index 98% rename from tests/low_level.cpp rename to tests/test_low_level.cpp index 1e57e3fd..0fb5b2e3 100644 --- a/tests/low_level.cpp +++ b/tests/test_low_level.cpp @@ -4,11 +4,10 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include -#include - +#include +#include +#include +#include #include #include #include @@ -18,9 +17,10 @@ #include #define BOOST_TEST_MODULE low level #include - -#include -#include +#include +#include +#include +#include // TODO: Test with empty strings. @@ -100,12 +100,15 @@ void test_sync(net::any_io_executor ex, expect e) ts.append(e.in); Result result; boost::system::error_code ec; - redis::detail::read(ts, net::dynamic_buffer(rbuffer), adapt2(result), ec); + auto dbuf = net::dynamic_buffer(rbuffer); + auto const consumed = redis::detail::read(ts, dbuf, adapt2(result), ec); if (e.ec) { BOOST_CHECK_EQUAL(ec, e.ec); return; } + dbuf.consume(consumed); + BOOST_TEST(!ec); BOOST_TEST(rbuffer.empty()); @@ -145,7 +148,7 @@ class async_test: public std::enable_shared_from_this> { } BOOST_TEST(!ec); - BOOST_TEST(self->rbuffer_.empty()); + //BOOST_TEST(self->rbuffer_.empty()); if (self->result_.has_value()) { auto const res = self->result_ == self->data_.expected; @@ -558,9 +561,9 @@ BOOST_AUTO_TEST_CASE(ignore_adapter_no_error) test_stream ts {ioc}; ts.append(S05b); - redis::detail::read(ts, net::dynamic_buffer(rbuffer), adapt2(ignore), ec); + auto const consumed = redis::detail::read(ts, net::dynamic_buffer(rbuffer), adapt2(ignore), ec); BOOST_TEST(!ec); - BOOST_TEST(rbuffer.empty()); + BOOST_CHECK_EQUAL(rbuffer.size(), consumed); } //----------------------------------------------------------------------------------- diff --git a/tests/cpp20_low_level_async.cpp b/tests/test_low_level_async.cpp similarity index 52% rename from tests/cpp20_low_level_async.cpp rename to tests/test_low_level_async.cpp index f357eb16..d05345fb 100644 --- a/tests/cpp20_low_level_async.cpp +++ b/tests/test_low_level_async.cpp @@ -4,12 +4,20 @@ * accompanying file LICENSE.txt) */ -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include #include +#define BOOST_TEST_MODULE conn-tls +#include +#if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; namespace redis = boost::redis; @@ -19,13 +27,14 @@ using boost::redis::adapter::adapt2; using net::ip::tcp; using boost::redis::request; using boost::redis::adapter::result; +using redis::config; -auto co_main(std::string host, std::string port) -> net::awaitable +auto co_main(config cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; resolver resv{ex}; - auto const addrs = co_await resv.async_resolve(host, port); + auto const addrs = co_await resv.async_resolve(cfg.addr.host, cfg.addr.port); tcp_socket socket{ex}; co_await net::async_connect(socket, addrs); @@ -40,13 +49,30 @@ auto co_main(std::string host, std::string port) -> net::awaitable std::string buffer; result resp; + std::size_t consumed = 0; // Reads the responses to all commands in the request. - auto dbuffer = net::dynamic_buffer(buffer); - co_await redis::detail::async_read(socket, dbuffer); - co_await redis::detail::async_read(socket, dbuffer, adapt2(resp)); - co_await redis::detail::async_read(socket, dbuffer); + auto dbuf = net::dynamic_buffer(buffer); + consumed = co_await redis::detail::async_read(socket, dbuf); + dbuf.consume(consumed); + consumed = co_await redis::detail::async_read(socket, dbuf, adapt2(resp)); + dbuf.consume(consumed); + consumed = co_await redis::detail::async_read(socket, dbuf); + dbuf.consume(consumed); std::cout << "Ping: " << resp.value() << std::endl; } +BOOST_AUTO_TEST_CASE(low_level_async) +{ + net::io_context ioc; + net::co_spawn(ioc, std::move(co_main({})), net::detached); + ioc.run(); +} + +#else // defined(BOOST_ASIO_HAS_CO_AWAIT) + +BOOST_AUTO_TEST_CASE(low_level_async) +{ +} + #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/tests/cpp17_low_level_sync.cpp b/tests/test_low_level_sync.cpp similarity index 63% rename from tests/cpp17_low_level_sync.cpp rename to tests/test_low_level_sync.cpp index 434441af..2349fbee 100644 --- a/tests/cpp17_low_level_sync.cpp +++ b/tests/test_low_level_sync.cpp @@ -4,30 +4,27 @@ * accompanying file LICENSE.txt) */ +#include +#include +#include +#include +#include +#define BOOST_TEST_MODULE conn-quit +#include #include #include -#include -#include -#include -#include - namespace net = boost::asio; namespace redis = boost::redis; using boost::redis::adapter::adapt2; using boost::redis::request; using boost::redis::adapter::result; -auto main(int argc, char * argv[]) -> int +BOOST_AUTO_TEST_CASE(low_level_sync) { try { - std::string host = "127.0.0.1"; - std::string port = "6379"; - - if (argc == 3) { - host = argv[1]; - port = argv[2]; - } + std::string const host = "127.0.0.1"; + std::string const port = "6379"; net::io_context ioc; net::ip::tcp::resolver resv{ioc}; @@ -45,11 +42,15 @@ auto main(int argc, char * argv[]) -> int std::string buffer; result resp; + std::size_t consumed = 0; // Reads the responses to all commands in the request. - auto dbuffer = net::dynamic_buffer(buffer); - redis::detail::read(socket, dbuffer); - redis::detail::read(socket, dbuffer, adapt2(resp)); - redis::detail::read(socket, dbuffer); + auto dbuf = net::dynamic_buffer(buffer); + consumed = redis::detail::read(socket, dbuf); + dbuf.consume(consumed); + consumed = redis::detail::read(socket, dbuf, adapt2(resp)); + dbuf.consume(consumed); + consumed = redis::detail::read(socket, dbuf); + dbuf.consume(consumed); std::cout << "Ping: " << resp.value() << std::endl; diff --git a/tests/test_low_level_sync_sans_io.cpp b/tests/test_low_level_sync_sans_io.cpp new file mode 100644 index 00000000..441f8098 --- /dev/null +++ b/tests/test_low_level_sync_sans_io.cpp @@ -0,0 +1,33 @@ +/* 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 +#include +#define BOOST_TEST_MODULE conn-quit +#include +#include +#include + +using boost::redis::adapter::adapt2; +using boost::redis::adapter::result; +using boost::redis::resp3::detail::deserialize; + +BOOST_AUTO_TEST_CASE(low_level_sync_sans_io) +{ + try { + result> resp; + + char const* wire = "~6\r\n+orange\r\n+apple\r\n+one\r\n+two\r\n+three\r\n+orange\r\n"; + deserialize(wire, adapt2(resp)); + + for (auto const& e: resp.value()) + std::cout << e << std::endl; + + } catch (std::exception const& e) { + std::cerr << e.what() << std::endl; + exit(EXIT_FAILURE); + } +} diff --git a/tests/request.cpp b/tests/test_request.cpp similarity index 91% rename from tests/request.cpp rename to tests/test_request.cpp index 8b5a603b..6dd29f04 100644 --- a/tests/request.cpp +++ b/tests/test_request.cpp @@ -10,9 +10,6 @@ #include #include -#include -#include -#include using boost::redis::request; diff --git a/tests/test_run.cpp b/tests/test_run.cpp new file mode 100644 index 00000000..4fbf6355 --- /dev/null +++ b/tests/test_run.cpp @@ -0,0 +1,100 @@ +/* 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 +#define BOOST_TEST_MODULE run +#include +#include +#include "common.hpp" + +namespace net = boost::asio; +namespace redis = boost::redis; + +using connection = redis::connection; +using redis::config; +using redis::logger; +using redis::operation; +using boost::system::error_code; +using namespace std::chrono_literals; + +bool is_host_not_found(error_code ec) +{ + if (ec == net::error::netdb_errors::host_not_found) return true; + if (ec == net::error::netdb_errors::host_not_found_try_again) return true; + return false; +} + +BOOST_AUTO_TEST_CASE(resolve_bad_host) +{ + net::io_context ioc; + + config cfg; + cfg.addr.host = "Atibaia"; + cfg.addr.port = "6379"; + cfg.resolve_timeout = 10h; + cfg.connect_timeout = 10h; + cfg.health_check_interval = 10h; + cfg.reconnect_wait_interval = 0s; + + auto conn = std::make_shared(ioc); + conn->async_run(cfg, {}, [](auto ec){ + BOOST_TEST(is_host_not_found(ec)); + }); + + ioc.run(); +} + +BOOST_AUTO_TEST_CASE(resolve_with_timeout) +{ + net::io_context ioc; + + config cfg; + cfg.addr.host = "occase.de"; + cfg.addr.port = "6379"; + cfg.resolve_timeout = 1ms; + cfg.connect_timeout = 1ms; + cfg.health_check_interval = 10h; + cfg.reconnect_wait_interval = 0s; + + auto conn = std::make_shared(ioc); + run(conn, cfg); + ioc.run(); +} + +BOOST_AUTO_TEST_CASE(connect_bad_port) +{ + net::io_context ioc; + + config cfg; + cfg.addr.host = "127.0.0.1"; + cfg.addr.port = "1"; + cfg.resolve_timeout = 10h; + cfg.connect_timeout = 10s; + cfg.health_check_interval = 10h; + cfg.reconnect_wait_interval = 0s; + + auto conn = std::make_shared(ioc); + run(conn, cfg, net::error::connection_refused); + ioc.run(); +} + +// Hard to test. +//BOOST_AUTO_TEST_CASE(connect_with_timeout) +//{ +// net::io_context ioc; +// +// config cfg; +// cfg.addr.host = "example.com"; +// cfg.addr.port = "80"; +// cfg.resolve_timeout = 10s; +// cfg.connect_timeout = 1ns; +// cfg.health_check_interval = 10h; +// +// auto conn = std::make_shared(ioc); +// run(conn, cfg, boost::redis::error::connect_timeout); +// ioc.run(); +//} +