diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ae9db989..ce2757ed 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,6 +21,7 @@ env: REGISTRY_OLD: docker.io BASE_IMAGE_REPO: python BASE_IMAGE_TAG: '3.12.1-slim-bookworm' + RMQCPP_CHECKOUT: cc6885319ccb97b8a6d13e09e83a52c43aab16c7 # narg/NARG (number-of-CPU argument to pass to the builds) are based on OS-specific CPU counts in the GitHub-hosted GHA runners. # checked 1/4/2024: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources @@ -55,14 +56,14 @@ jobs: - name: Set up Docker Buildx id: setup_buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: driver: docker - name: Login to GHCR # This condition should match the `push` condition in the `Build` step just below if: ${{ github.event.inputs.keep-test-image == 'true' }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -70,7 +71,7 @@ jobs: - name: Build id: build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . push: ${{ github.event.inputs.keep-test-image == 'true' }} @@ -83,12 +84,13 @@ jobs: build_examples=TRUE enable_testing=TRUE narg=${{ env.NARG }} + rmqcpp_checkout=${{ env.RMQCPP_CHECKOUT }} platforms: linux/amd64 tags: ${{ env.DL_CPP_TAG }} - name: Build Integration Tests id: build_int_tests - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ./testing/integration push: false @@ -109,7 +111,8 @@ jobs: - name: Integration Tests run: | cd testing/integration - ./do-testing.sh ${{ env.INT_TAG }} + ./run-dl-tests.sh ${{ env.INT_TAG }} + ./run-broker-restart-test.sh ${{ env.INT_TAG }} # For debugging # - name: Setup tmate session @@ -119,6 +122,9 @@ jobs: test_builds: + # temporarily disable + if: false + runs-on: ${{ matrix.os }} # This job runs for all events that trigger this workflow @@ -130,8 +136,10 @@ jobs: include: - os: ubuntu-22.04 narg: 2 + triplet: x64-linux-release - os: macos-latest narg: 3 + triplet: arm64-osx-release steps: @@ -154,7 +162,6 @@ jobs: if: startsWith(matrix.os, 'macos') run: | brew install \ - boost \ rabbitmq-c \ rapidjson \ yaml-cpp @@ -164,10 +171,46 @@ jobs: run: | sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ - libboost-all-dev \ librabbitmq-dev \ rapidjson-dev \ libyaml-cpp-dev + + - name: Install dependencies -- common + run: | + ${VCPKG_INSTALLATION_ROOT}/vcpkg install --triplet ${{ matrix.triplet }} \ + boost-filesystem \ + boost-system \ + boost-chrono \ + boost-variant \ + boost-uuid + git clone https://github.com/bloomberg/rmqcpp.git + cd rmqcpp + git checkout ${{ env.RMQCPP_CHECKOUT }} + ${VCPKG_INSTALLATION_ROOT}/vcpkg install --triplet ${{ matrix.triplet }} + mkdir -p build + cd build + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_TESTING=OFF \ + .. + make -j${narg} install + + +# mkdir -p /usr/local/share/libpcre2-8 +# printf '%s\n' \ +# 'include("/usr/local/share/pcre2/pcre2-config.cmake")' \ +# 'if(NOT TARGET libpcre2-8::pcre2-8 AND TARGET pcre2::pcre2-8-static)' \ +# ' add_library(libpcre2-8::pcre2-8 ALIAS pcre2::pcre2-8-static)' \ +# 'endif()' \ +# 'set(libpcre2-8_FOUND TRUE)' \ +# > /usr/local/share/libpcre2-8/libpcre2-8Config.cmake +# RMQCPP_CONFIG=$(find /usr/local -name rmqcppConfig.cmake 2>/dev/null | head -1) +# test -n "${RMQCPP_CONFIG}" +# if ! grep -q 'find_dependency(zstd' "${RMQCPP_CONFIG}"; then \ +# sed -i '/include.*rmqcppTargets/i find_dependency(zstd CONFIG)' "${RMQCPP_CONFIG}"; \ +# fi - name: Configure run: | @@ -186,9 +229,9 @@ jobs: ./run_dl_tests # For debugging -# - name: Setup tmate session -# if: ${{ ! success() }} -# uses: mxschmitt/action-tmate@v3 + - name: Setup tmate session + if: ${{ ! success() }} + uses: mxschmitt/action-tmate@v3 build_and_push: @@ -238,7 +281,7 @@ jobs: - name: Docker meta id: docker_meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ env.REGISTRY_OLD }}/${{ github.repository }} @@ -255,7 +298,7 @@ jobs: - name: Docker meta - integration id: docker_meta_integration if: ${{ matrix.do-integration }} - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ env.REGISTRY }}/${{ github.repository }} @@ -273,19 +316,19 @@ jobs: - name: Set up Docker Buildx id: setup_buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: buildkitd-flags: --debug - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY_OLD }} username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -293,7 +336,7 @@ jobs: - name: Build and push id: build_push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . push: ${{ github.event_name == 'push' }} # limited to develop, main, and tags; don't push on PR @@ -303,14 +346,15 @@ jobs: build_type=${{ matrix.build-type }} build_examples=${{ matrix.build-examples }} enable_testing=${{ matrix.enable-testing }} - narg=${{env.NARG }} + narg=${{ env.NARG }} + rmqcpp_checkout=${{ env.RMQCPP_CHECKOUT }} tags: ${{ steps.docker_meta.outputs.tags }} platforms: linux/amd64,linux/arm64,linux/arm/v7 - name: Build and push - integration id: build_push_integration if: ${{ matrix.do-integration }} - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ./testing/integration push: ${{ github.event_name == 'push' }} # limited to develop, main, and tags; don't push on PR diff --git a/.gitmodules b/.gitmodules index c1f3202a..1ca03f96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "scarab"] path = scarab url = https://github.com/project8/scarab -[submodule "external/SimpleAmqpClient"] - path = external/SimpleAmqpClient - url = https://github.com/project8/SimpleAmqpClient diff --git a/AGENTS.md b/AGENTS.md index 4e29b635..32e6891d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,9 +18,9 @@ The goal is to make safe, minimal, style-consistent changes to dripline-cpp. - `core` owns AMQP connectivity and send/listen primitives. - `message` and derived types (`msg_request`, `msg_reply`, `msg_alert`) implement protocol objects and chunking. -- `receiver` and `listener` manage chunk assembly and concurrent processing. +- `receiver` manages chunk assembly; `message_dispatcher` (formerly `concurrent_receiver`) holds the rmqcpp Consumer and dispatches assembled messages. - `endpoint` implements request dispatch and lockout semantics. -- `service` composes endpoint + listener/receiver + heartbeater + scheduler. +- `service` composes endpoint + message_dispatcher + heartbeater + scheduler. - `hub` maps message specifiers to user-registered handlers. - `agent` and `monitor` provide CLI-oriented message send/observe tooling. diff --git a/CMakeLists.txt b/CMakeLists.txt index f9380d65..2202de9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,11 @@ cmake_minimum_required (VERSION 3.12) # setup # ######### +if( DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) + set( CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ) + message( STATUS "Setting vcpkg toolchain file: ${CMAKE_TOOLCHAIN_FILE}" ) +endif() + cmake_policy( SET CMP0048 NEW ) # version in project() project( Dripline VERSION 2.10.12 ) @@ -75,57 +80,14 @@ if( Dripline_ENABLE_TESTING ) endif() -#################### -# SimpleAmqpClient # -#################### +########## +# rmqcpp # +########## -# SimpleAmqpClient needs to be built independently from, but as a submodule and dependency of, dripline-cpp. -# In other words, the configuration and build environments need to be kept separate. - -# First we run the configure stage of SimpleAmqpClient during the configure (cmake) process of dripline-cpp. -# This build takes place in its own build directory, but it installs in the same place as dripline-cpp. -# We do this here instead of below in the ExternalProject so that dripline-cpp can learn about the SimpleAmqpClient targets at the configure stage. - -if( NOT EXISTS ${PROJECT_BINARY_DIR}/external ) - execute_process( - COMMAND mkdir external - WORKING_DIRECTORY ${PROJECT_BINARY_DIR} - ) -endif() - -execute_process( - COMMAND cmake -D CMAKE_INSTALL_PREFIX:STRING=${CMAKE_INSTALL_PREFIX} - -D PACKAGE_CONFIG_PREFIX:STRING=${PACKAGE_CONFIG_PREFIX} - -D TOP_PROJECT_CMAKE_CONFIG_DIR:STRING=${TOP_PROJECT_CMAKE_CONFIG_DIR} - -D LIB_INSTALL_DIR:STRING=${LIB_INSTALL_DIR} - -D BIN_INSTALL_DIR:STRING=${BIN_INSTALL_DIR} - -D CMAKE_BUILD_TYPE=RELEASE - -D CMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} - ${PROJECT_SOURCE_DIR}/external - WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/external -) - -# Second we use the ExternalProject module to build and install SimpleAmqpClient. -# This takes place during the build phase of dripline-cpp. -# Note that we disable the configure stage of the ExternalProject because we already took care of the configure stage above. -include(ExternalProject) - -ExternalProject_Add( DriplineExternal - PREFIX ${CMAKE_INSTALL_PREFIX} - SOURCE_DIR ${PROJECT_SOURCE_DIR}/external - BINARY_DIR ${PROJECT_BINARY_DIR}/external - CONFIGURE_COMMAND "" - BUILD_ALWAYS 1 - BUILD_COMMAND $(MAKE) - INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - INSTALL_COMMAND $(MAKE) install -) - -# Finally we use find_package() to learn about the Kassiopeia build targets. -find_package( SimpleAmqpClient REQUIRED CONFIG HINTS ${PROJECT_BINARY_DIR}/external ) +find_package( rmqcpp REQUIRED ) # Added to the public libraries so that downstream code can successfully get the necessary include directories -list( APPEND PUBLIC_EXT_LIBS SimpleAmqpClient::SimpleAmqpClient ) +list( APPEND PUBLIC_EXT_LIBS rmqcpp::rmq ) ################## @@ -143,10 +105,6 @@ else( Dripline_BUILD_PYTHON ) set_option( Scarab_BUILD_PYTHON FALSE ) endif( Dripline_BUILD_PYTHON ) -# SimpleAmqpClient needs system and chrono -list( APPEND Scarab_BOOST_COMPONENTS chrono ) - - ##################### # Prepare for Build # ##################### @@ -195,7 +153,7 @@ endif() ######### if( Dripline_ENABLE_TESTING ) - add_subdirectory( testing ) + add_subdirectory( testing/unit ) endif() ################## diff --git a/Dockerfile b/Dockerfile index 6e27a384..edcf2f52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,10 @@ ARG build_examples=FALSE ARG enable_testing=FALSE ARG narg=2 -# Most dependencies +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +ENV VCPKG_ROOT=/usr/local/vcpkg +# Most dependencies RUN apt-get update && \ apt-get clean && \ apt-get --fix-missing -y install \ @@ -18,12 +20,17 @@ RUN apt-get update && \ cmake \ # gdb \ git \ - libboost-chrono-dev \ - libboost-filesystem-dev \ - libboost-system-dev \ - librabbitmq-dev \ libyaml-cpp-dev \ - rapidjson-dev && \ + rapidjson-dev \ + libssl-dev \ + zlib1g-dev \ + libzstd-dev \ + ninja-build \ + pkg-config \ + curl \ + tar \ + unzip \ + zip && \ # pybind11-dev \ # wget && \ rm -rf /var/lib/apt/lists/* @@ -33,7 +40,7 @@ ARG pybind11_checkout=v3.0.1 ARG pybind11_repo=https://github.com/pybind/pybind11.git ARG pybind11_name=pybind11 RUN cd /usr/local && \ - git clone ${pybind11_repo} && \ + git clone ${pybind11_repo} ${pybind11_name} && \ cd ${pybind11_name} && \ git checkout ${pybind11_checkout} && \ mkdir build && \ @@ -43,6 +50,60 @@ RUN cd /usr/local && \ cd / && \ rm -rf /usr/local/${pybind11_name} +ARG TARGETARCH +ARG rmqcpp_checkout=cc6885319ccb97b8a6d13e09e83a52c43aab16c7 +RUN cd /usr/local && \ + git clone https://github.com/Microsoft/vcpkg.git && \ + /usr/local/vcpkg/bootstrap-vcpkg.sh && \ + git clone https://github.com/bloomberg/rmqcpp.git && \ + cd /usr/local/rmqcpp && \ + git checkout ${rmqcpp_checkout} && \ + case "${TARGETARCH}" in \ + amd64) TRIPLET="x64-linux-release" ;; \ + arm64) TRIPLET="arm64-linux-release" ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;; \ + esac && \ + ${VCPKG_ROOT}/vcpkg install --triplet ${TRIPLET} && \ + mkdir -p /usr/local/rmqcpp/build && \ + cd /usr/local/rmqcpp/build && \ + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DVCPKG_TARGET_TRIPLET=${TRIPLET} \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_TESTING=OFF \ + .. && \ + make -j${narg} install && \ + cd / && \ + ${VCPKG_ROOT}/vcpkg install --triplet ${TRIPLET} \ + boost-filesystem \ + boost-system \ + boost-chrono \ + boost-variant \ + boost-uuid && \ + cp -a /usr/local/rmqcpp/vcpkg_installed/${TRIPLET}/include/. /usr/local/include/ && \ + cp -a /usr/local/rmqcpp/vcpkg_installed/${TRIPLET}/lib/. /usr/local/lib/ && \ + cp -a /usr/local/rmqcpp/vcpkg_installed/${TRIPLET}/share/. /usr/local/share/ && \ + cp -a ${VCPKG_ROOT}/installed/${TRIPLET}/include/. /usr/local/include/ && \ + cp -a ${VCPKG_ROOT}/installed/${TRIPLET}/lib/. /usr/local/lib/ && \ + cp -a ${VCPKG_ROOT}/installed/${TRIPLET}/share/. /usr/local/share/ && \ + rm -rf /usr/local/share/zstd && \ + # Create a shim file to adapt the vcpkg install of "pcre2" to the expected standard "libpcre2-8" + mkdir -p /usr/local/share/libpcre2-8 && \ + printf '%s\n' \ + 'include("/usr/local/share/pcre2/pcre2-config.cmake")' \ + 'if(NOT TARGET libpcre2-8::pcre2-8 AND TARGET pcre2::pcre2-8-static)' \ + ' add_library(libpcre2-8::pcre2-8 ALIAS pcre2::pcre2-8-static)' \ + 'endif()' \ + 'set(libpcre2-8_FOUND TRUE)' \ + > /usr/local/share/libpcre2-8/libpcre2-8Config.cmake && \ + # rmqcpp's rmqcppConfig.cmake is missing the use of find_dependency(zstd), so we add it + RMQCPP_CONFIG=$(find /usr/local -name rmqcppConfig.cmake 2>/dev/null | head -1) && \ + test -n "${RMQCPP_CONFIG}" && \ + if ! grep -q 'find_dependency(zstd' "${RMQCPP_CONFIG}"; then \ + sed -i '/include.*rmqcppTargets/i find_dependency(zstd CONFIG)' "${RMQCPP_CONFIG}"; \ + fi + #rm -rf /usr/local/rmqcpp /usr/local/vcpkg /root/.cache/vcpkg + FROM base AS devel RUN apt-get update && \ @@ -58,7 +119,6 @@ FROM base # note that the build dir is *not* in source, this is so that the source can me mounted onto the container without covering the build target COPY .git /usr/local/src/.git -COPY external /usr/local/src/external COPY documentation /usr/local/src/documentation COPY scarab /usr/local/src/scarab COPY library /usr/local/src/library @@ -74,7 +134,7 @@ RUN mkdir -p /usr/local/build && \ cmake ../src && \ # unclear why I have to run cmake twice cmake -DCMAKE_BUILD_TYPE=${build_type} \ - -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ + -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ -DDripline_BUILD_EXAMPLES:BOOL=${build_examples} \ -DDripline_ENABLE_TESTING:BOOL=${enable_testing} \ -DDripline_BUILD_PYTHON:BOOL=TRUE \ diff --git a/DockerfileBase b/DockerfileBase deleted file mode 100644 index 3d233fde..00000000 --- a/DockerfileBase +++ /dev/null @@ -1,46 +0,0 @@ -#FROM debian:9 -FROM amd64/python:3.7 - -# Most dependencies - -RUN apt-get update && \ - apt-get clean && \ - apt-get --fix-missing -y install \ - build-essential \ - cmake \ - gdb \ - git \ - libboost-chrono-dev \ - libboost-filesystem-dev \ - libboost-system-dev \ - librabbitmq-dev \ - wget && \ - rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /usr/local/deps && \ - git clone https://github.com/jbeder/yaml-cpp && \ - cd yaml-cpp && \ - mkdir build && \ - cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release \ - -DYAML_CPP_BUILD_CONTRIB:BOOL=FALSE \ - -DYAML_CPP_BUILD_TOOLS:BOOL=FALSE \ - -DYAML_CPP_BUILD_TESTS:BOOL=FALSE \ - -DYAML_BUILD_SHARED_LIBS=TRUE \ - .. && \ - make install - -RUN cd /usr/local/deps && \ - git clone https://github.com/Tencent/rapidjson && \ - cd rapidjson && \ - mkdir build && \ - cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release \ - -DRAPIDJSON_BUILD_DOC=FALSE \ - -DRAPIDJSON_BUILD_EXAMPLES=FALSE \ - -DRAPIDJSON_BUILD_TESTS=FALSE \ - -DRAPIDJSON_BUILD_THIRDPARTY_GTEST=FALSE \ - .. && \ - make install - - diff --git a/DriplineConfig.cmake.in b/DriplineConfig.cmake.in index 4defc0f3..14ddc0bd 100644 --- a/DriplineConfig.cmake.in +++ b/DriplineConfig.cmake.in @@ -6,7 +6,7 @@ get_filename_component( Dripline_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH ) include( CMakeFindDependencyMacro ) find_dependency( Scarab REQUIRED PATHS ${Dripline_CMAKE_DIR}/Scarab @Scarab_BINARY_LOCATION@ ) -find_dependency( SimpleAmqpClient REQUIRED PATHS ${Dripline_CMAKE_DIR}/SimpleAmqpClient ${Dripline_CMAKE_DIR}/external ) +find_dependency( rmqcpp REQUIRED ) if( NOT TARGET Dripline::@Dripline_FULL_TARGET_NAME@ ) if( TARGET @Dripline_FULL_TARGET_NAME@ ) diff --git a/changelog.md b/changelog.md index 75971336..875e22f8 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,21 @@ Types of changes: Added, Changed, Deprecated, Removed, Fixed, Security ## [Unreleased] +### Changed + +- Replaced SimpleAmqpClient with rmqcpp (Bloomberg) as the AMQP backend. + - `concurrent_receiver` renamed to `message_dispatcher`; extracted into `library/message_dispatcher.hh` / `library/message_dispatcher.cc`. + - `listener` and `listener_receiver` classes deleted; Consumer lifecycle absorbed into `message_dispatcher`. + - Message delivery is now callback-based (rmqcpp thread pool); dripline-cpp no longer manages listener or receiver threads. + - Stale incomplete multi-chunk messages are lazily evicted after `single_message_wait_ms` ms. + - `core::send()` no longer accepts an `amqp_channel_ptr` parameter (breaking API change). + - `receiver::wait_for_reply()` now uses a `std::future` instead of a polling loop. + +### Removed + +- `library/listener.hh` and `library/listener.cc` (deleted). +- `external/SimpleAmqpClient/` submodule (deleted). + ## [2.10.12] -- 2026-06-05 diff --git a/documentation/source/building.rst b/documentation/source/building.rst index 28b03a06..fea826f6 100644 --- a/documentation/source/building.rst +++ b/documentation/source/building.rst @@ -33,6 +33,12 @@ Dripline build options include: ``Dripline_MAX_PAYLOAD_SIZE`` (INT) Maximum payload size in bytes + ``Dripline_OFFLINE`` (BOOL) + if ON, builds the library in offline mode (no broker connection required). + In this mode, sending a message throws the message object rather than transmitting it, + which is useful for unit testing. + See :ref:`Offline & No-Connection ` for more details. + ``Dripline_PYTHON_THROW_REPLY_KEYWORD`` keyword used by the python wrapping that indicates that the thrown object is a reply message to be sent diff --git a/documentation/source/configuration.rst b/documentation/source/configuration.rst index fd1b167f..f94becc6 100644 --- a/documentation/source/configuration.rst +++ b/documentation/source/configuration.rst @@ -34,7 +34,7 @@ Here is the possible set of parameters for the ``dripline_mesh`` block, with a s loop_timeout_ms: (unsigned int) time used in loops for checking for application shutdown in milliseconds message_wait_ms: (unsigned int) timeout for waiting for a message in milliseconds heartbeat_routing_key: (string) routing key for sending and receiving heartbeat messages - hearteat_interval_s: (unsigned int) interval for sending heartbeats in seconds + heartbeat_interval_s: (unsigned int) interval for sending heartbeats in seconds return_codes: - name: (string) return-code name (must be unique) value: (unsigned int) return-code value (must be unique) @@ -57,7 +57,7 @@ The defaults for all of these parameters are given in the class ``dripline_confi loop_timeout_ms: 1000 message_wait_ms: 1000 heartbeat_routing_key: heartbeat - hearteat_interval_s: 60 + heartbeat_interval_s: 60 .. _default-mesh-yaml: diff --git a/documentation/source/dl_agent.rst b/documentation/source/dl_agent.rst index c56214ce..8006e9ec 100644 --- a/documentation/source/dl_agent.rst +++ b/documentation/source/dl_agent.rst @@ -37,8 +37,8 @@ Options --requests-exchange TEXT Set the name of the requests exchange --alerts-exchange TEXT Set the name of the alerts exchange --max-payload UINT Set the maximum payload size (in bytes) - --loop-timeout-msdripline.loop-timeout-ms UINT - --message-wait-msdripline.message-wait-ms UINT + --loop-timeout-ms UINT Set the loop timeout in ms + --message-wait-ms UINT Set the message wait time in ms --heartbeat-routing-key TEXT Set the first token of heartbeat routing keys: [token].[origin] --heartbeat-interval-s UINT Set the interval between heartbeats in s -s,--specifier TEXT Set the specifier diff --git a/documentation/source/dl_mon.rst b/documentation/source/dl_mon.rst index 59ad6739..aaed4f69 100644 --- a/documentation/source/dl_mon.rst +++ b/documentation/source/dl_mon.rst @@ -32,8 +32,8 @@ Options --requests-exchange TEXT Set the name of the requests exchange --alerts-exchange TEXT Set the name of the alerts exchange --max-payload UINT Set the maximum payload size (in bytes) - --loop-timeout-msdripline.loop-timeout-ms UINT - --message-wait-msdripline.message-wait-ms UINT + --loop-timeout-ms UINT Set the loop timeout in ms + --message-wait-ms UINT Set the message wait time in ms --heartbeat-routing-key TEXT Set the first token of heartbeat routing keys: [token].[origin] --heartbeat-interval-s UINT Set the interval between heartbeats in s diff --git a/documentation/source/getting_started.rst b/documentation/source/getting_started.rst index 48e167cc..c62f11c9 100644 --- a/documentation/source/getting_started.rst +++ b/documentation/source/getting_started.rst @@ -15,8 +15,9 @@ Requirements * CMake 3.12 * C++17 -* Boost 1.46 -* rabbitmqc +* Boost 1.46 (including Boost.Asio) +* `rmqcpp `_ (Bloomberg AMQP client library) +* BDE libraries (Bloomberg Development Environment; required by rmqcpp) * libyaml-cpp * rapidjson * quill (logging library) @@ -25,7 +26,6 @@ Requirements Included Packages ================= -* `SimpleAmqpClient `_ (unchanged fork of `upstream SimpleAmqpClient `_) * `scarab `_ diff --git a/documentation/source/library.rst b/documentation/source/library.rst index f9d4708b..4265de7f 100644 --- a/documentation/source/library.rst +++ b/documentation/source/library.rst @@ -21,8 +21,8 @@ There are also a number of classes that implement various features of the above * :ref:`Core`: interface for the RabbitMQ client library; interface includes interacting with the broker and for sending and receiving messages * :ref:`Heartbeater`: implements the heartbeat behavior -* :ref:`Listeners`: listen for AMQP messages (dripline message chunks or whole messages) -* :ref:`Receivers`: collect dripline message chunks and assembles them into complete messages +* :ref:`Message Dispatcher`: manages the rmqcpp Consumer lifecycle and dispatches assembled Dripline messages +* :ref:`Receiver`: collects Dripline message chunks and assembles them into complete messages * :ref:`Scheduler`: executes scheduled events * :ref:`Specifier`: parses specifier strings * :ref:`Version Store`: stores version information for a particular application context @@ -61,7 +61,7 @@ It's range of capabilities are largely defined by the classes it inherits from: * ``core`` * ``endpoint`` -* ``listener_receiver`` +* ``message_dispatcher`` * ``heartbeater`` * ``scheduler`` @@ -74,9 +74,15 @@ The interface for running a service consists of three functions: Or you can use ``run()`` to perform the start-->listen-->stop sequence. A service can have both synchronous and asynchronous child endpoints. With the former, requests are -handled synchronously with the recieving of messages and with processing messages bound for itself. +handled synchronously with the receiving of messages and with processing messages bound for itself. With the latter, requests are passed to the appropriate endpoint, which handles them in its own thread. +Message delivery is handled by rmqcpp's internal thread pool via callbacks. Dripline-cpp manages +only the following threads: + +* **Heartbeat thread** — sends regular heartbeat messages (optional) +* **Scheduler thread** — executes scheduled events (optional) + .. _messages: Messages @@ -132,7 +138,7 @@ Core ---- The ``core`` class provides an interface for the basic AMQP functionality. It wraps the -more general RabbitMQ API in a dripline-specific interface. +rmqcpp RabbitMQ API in a dripline-specific interface. The class includes a number of static utility functions for interacting with the broker. @@ -150,42 +156,52 @@ The heartbeat is an alert sent to a pre-determined routing key, which is given a ``execute()`` function. The interval for sending the heartbeats is ``f_heartbeat_interval_s``, which is in seconds. The default interval is 60 s. -.. _listeners: +.. _message-dispatcher: -Listeners ---------- +Message Dispatcher +------------------ + +The ``message_dispatcher`` class manages the lifecycle of an rmqcpp Consumer +(via ``start_listening()`` / ``stop_listening()``) and dispatches each assembled +Dripline message to ``submit_message()``. -A listener is a class capable of listening on an AMQP channel for AMQP messages, -which represent either a dripline message chunk or an entire dripline message. -The ``listener`` class provides the basic framework for doing that. +Message delivery is callback-based: rmqcpp's internal thread pool invokes the delivery +callback, which passes each AMQP message chunk to ``receiver::handle_message_chunk()``. +Once all chunks of a Dripline message have arrived, the assembled message is dispatched +synchronously to ``submit_message()`` in the rmqcpp callback thread. -The typical use case involves at least two threads: -1. A listener gets messages from the AMQP channel (using ``listen_on_queue()``, - e.g. ``service`` or ``endpoint_listener_receiver``) and - calls ``receiver::handle_message_chunk()`` -2. A receiver has a timing thread waiting for multiple message chunks (if relevant); - when the message is complete, ``receiver::process_message()`` is called. +A class deriving from ``message_dispatcher`` must implement ``submit_message()`` to define +what happens with each received message. The two concrete implementations in dripline-cpp are: -``listener_receiver`` is a convenience class that brings together ``listener`` and ``concurrent_receiver``. +* ``service`` — dispatches messages to itself or to its child endpoints +* ``endpoint_listener_receiver`` — a decorator class that wraps a plain ``endpoint`` and adds + ``message_dispatcher`` capabilities, allowing it to act as an asynchronous child endpoint of a ``service`` -``endpoint_listener_receiver`` is a decorator class for a "plain" endpoint: -it adds ``listener_receiver`` capabilities, allowing it to act as an asynchronous endpoint of a ``service``. +.. note:: + Prior to the rmqcpp migration, this class was named ``concurrent_receiver``. The name was changed + because the class no longer manages any concurrency itself — rmqcpp's thread pool handles delivery. .. _receivers: -Receivers ---------- +Receiver +-------- A receiver is able to collect Dripline message chunks and reassemble them into a complete dripline message. Dripline messages can be broken up into multiple chunks, each of which is transported as an AMQP message. -A receiver is responsible for handling message chunks, storing incomplete dripline messages, and eventually +A receiver is responsible for handling message chunks, storing incomplete dripline messages, and eventually processing complete dripline messages. -The ``receiver`` class contains an interface specifically for users waiting to receive reply messages: `wait_for_reply()`. +When a message chunk arrives via ``handle_message_chunk()``, it is stored in the incoming-message map. +Message chunks for a given message can be received in any order. Once all chunks for a message have +arrived, ``process_message_pack()`` is called inline (no separate thread is spawned). + +Stale incomplete messages (entries older than ``single_message_wait_ms`` ms) are lazily evicted at the +start of each ``handle_message_chunk()`` call. -The ``concurrent_receiver`` class allows client code to concurrently receive and process messages -(i.e. in separate threads). +The ``receiver`` class contains an interface specifically for users waiting to receive reply messages: +``wait_for_reply()``. This uses a ``std::future`` to wait for the reply, which is fulfilled by the +reply consumer's callback when the reply arrives. .. _scheduler: diff --git a/documentation/source/offline.rst b/documentation/source/offline.rst index 311c0cd2..12c6b3e3 100644 --- a/documentation/source/offline.rst +++ b/documentation/source/offline.rst @@ -8,14 +8,13 @@ Offline ------- This mode is a build-time option that is typically used for unit-testing purposes. +It is enabled by setting the ``Dripline_OFFLINE`` CMake flag (see :ref:`Build Options `). In this mode: * Sending a message results in the message object being thrown (in the C++ sense). This feature is useful for testing the various send-message options. -* Receiving messages is currently disabled, as is any other function that uses the SimpleAMQP library API. Such methods will respond with error conditions. Some message-receive functionality could be tested using the Offline mode in the future, e.g. by manually submitting a SimpleAMQP envelope object. - -Offline mode is selected by building the library with the ``Dripline_OFFLINE`` flag enabled in CMake. +* Receiving messages is currently disabled, as is any other function that uses the AMQP library (rmqcpp) API. Such methods will respond with error conditions. Some message-receive functionality could be tested using the Offline mode in the future, e.g. by manually submitting an AMQP envelope object. No-Connection ------------- @@ -24,6 +23,6 @@ Some dripline classes have flags in the constructors that allow them to be used Depending on how it's used, this could be done as either a build-time or a run-time option. -For an object in no-connection mode, methods that use the SimpleAMQP API will respond with error conditions. +For an object in no-connection mode, methods that use the AMQP library (rmqcpp) API will respond with error conditions. This feature is useful, for example, when a user wants to be able to handle message objects that are not received from a broker (e.g. they're created manually). diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt deleted file mode 100644 index b3c96a89..00000000 --- a/external/CMakeLists.txt +++ /dev/null @@ -1,54 +0,0 @@ -# This is a wrapper project used to build any external dependencies that require wrapping. -# -# SimpleAmqpClient requires wrapping so that modern CMake practices (use of targets and interfaces) can be applied. - -# Minimum cmake verison 3.1 required by dripline-cpp -cmake_minimum_required (VERSION 3.12) - -cmake_policy( SET CMP0048 NEW ) # version in project() -project( DriplineExternal VERSION 0.0.0 ) - -################## -# SimpleAmqpClient -################## - -option( BUILD_API_DOCS "" OFF ) -add_subdirectory( SimpleAmqpClient ) - -# Because the SimpleAmqpClient CMake build doesn't take care of this stuff, we have to do it ourselves - -# Set interface include directories -set_property( TARGET SimpleAmqpClient - APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES - "$" - "$/include>" -) - -# Build-interface targets -export( TARGETS SimpleAmqpClient - NAMESPACE SimpleAmqpClient:: - FILE ${CMAKE_CURRENT_BINARY_DIR}/SimpleAmqpClientTargets.cmake -) - -# Install-interface targets -# The SimpleAmqpClientTargets export was added to SimpleAmqpClient in the project8 fork -# Install SimpleAmqpClientTargets.cmake in the Dripline package-config install directory -install( EXPORT SimpleAmqpClientTargets - FILE - SimpleAmqpClientTargets.cmake - NAMESPACE - SimpleAmqpClient:: - DESTINATION - ${PACKAGE_CONFIG_PREFIX}/SimpleAmqpClient -) - -# Process SimpleAmqpClient.cmake.in -configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/SimpleAmqpClientConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/SimpleAmqpClientConfig.cmake @ONLY ) - -install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/SimpleAmqpClientConfig.cmake - DESTINATION - ${TOP_PROJECT_CMAKE_CONFIG_DIR}/SimpleAmqpClient -) - diff --git a/external/SimpleAmqpClient b/external/SimpleAmqpClient deleted file mode 160000 index 84ac3883..00000000 --- a/external/SimpleAmqpClient +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 84ac388383dc82b8fad542317fdc106f7412aca3 diff --git a/external/SimpleAmqpClientConfig.cmake.in b/external/SimpleAmqpClientConfig.cmake.in deleted file mode 100644 index b7a0aa90..00000000 --- a/external/SimpleAmqpClientConfig.cmake.in +++ /dev/null @@ -1,16 +0,0 @@ -# SimpleAmqpClientConfig.cmake - -get_filename_component( SimpleAmqpClient_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH ) - -# Find the dependencies -include( CMakeFindDependencyMacro ) -find_dependency( Boost 1.46 REQUIRED COMPONENTS chrono ) - -# Import library targets if it's not already present (usable at build time or as installed) -if( NOT TARGET SimpleAmqpClient::SimpleAmqpClient ) - if( TARGET SimpleAmqpClient ) - add_library( SimpleAmqpClient::SimpleAmqpClient ALIAS SimpleAmqpClient ) - else() - include( "${SimpleAmqpClient_CMAKE_DIR}/SimpleAmqpClientTargets.cmake" ) - endif() -endif() diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 5a85083b..b4baf9b9 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -22,8 +22,8 @@ set( dripline_HEADERS endpoint.hh heartbeater.hh hub.hh - listener.hh message.hh + message_dispatcher.hh monitor.hh monitor_config.hh receiver.hh @@ -49,8 +49,8 @@ set( dripline_SOURCES endpoint.cc heartbeater.cc hub.cc - listener.cc message.cc + message_dispatcher.cc monitor.cc monitor_config.cc receiver.cc diff --git a/library/README.md b/library/README.md index b51bb8b4..d5461465 100644 --- a/library/README.md +++ b/library/README.md @@ -1,3 +1,22 @@ -# dripline-cpp/source +# dripline-cpp/library -This directory contains all of the source files for dripline-cpp, both executables and the library. +This directory contains all of the source files for the dripline-cpp library. + +## Key Classes + +| Class | Files | Description | +|---|---|---| +| `core` | `core.hh` / `core.cc` | AMQP connectivity, send primitives, VHost/Topology management | +| `message`, `msg_request`, `msg_reply`, `msg_alert` | `message.hh` / `message.cc` | Protocol objects, serialization, chunking | +| `receiver` | `receiver.hh` / `receiver.cc` | Chunk assembly, incoming-message map, stale-entry eviction, `wait_for_reply()` | +| `message_dispatcher` | `message_dispatcher.hh` / `message_dispatcher.cc` | rmqcpp Consumer lifecycle (`start_listening` / `stop_listening`), dispatches assembled messages to `submit_message()`. Formerly named `concurrent_receiver`. | +| `endpoint` | `endpoint.hh` / `endpoint.cc` | Request dispatch, lockout semantics | +| `endpoint_listener_receiver` | `endpoint.hh` / `endpoint.cc` | Decorator: wraps an `endpoint` with `message_dispatcher` capabilities for async child use | +| `service` | `service.hh` / `service.cc` | Composes `core` + `endpoint` + `message_dispatcher` + `heartbeater` + `scheduler` | +| `monitor` | `monitor.hh` / `monitor.cc` | Composes `core` + `message_dispatcher`; prints received messages | +| `hub` | `hub.hh` / `hub.cc` | Maps specifiers to user-registered handler functions | +| `agent` | `agent.hh` / `agent.cc` | CLI-oriented one-shot message sender | +| `heartbeater` | `heartbeater.hh` / `heartbeater.cc` | Periodic heartbeat sender | +| `scheduler` | `scheduler.hh` | Event scheduler | +| `relayer` | `relayer.hh` / `relayer.cc` | Forwards messages between services | +| `reply_cache` | `reply_cache.hh` / `reply_cache.cc` | Python-binding reply cache (built only with `Scarab_BUILD_PYTHON`) | diff --git a/library/agent.cc b/library/agent.cc index 88002b66..d0c4324b 100644 --- a/library/agent.cc +++ b/library/agent.cc @@ -81,7 +81,7 @@ namespace dripline t_config.remove( "auth_file" ); t_config.remove( "auth_groups" ); - f_agent->set_timeout( t_config.get_value( "timeout", 10U ) * 1000 ); // convert seconds (dripline agent user interface) to milliseconds (expected by SimpleAmqpClient) + f_agent->set_timeout( t_config.get_value( "timeout", 10U ) * 1000 ); // convert seconds (dripline agent user interface) to milliseconds t_config.erase( "timeout" ); f_agent->set_json_print( t_config.get_value( "json_print", f_agent->get_json_print() ) ); t_config.erase( "json_print" ); @@ -208,16 +208,15 @@ namespace dripline return; } - if( ! t_receive_reply->f_consumer_tag.empty() ) // this indicates that the reply queue was created, and we've started consuming on it; we should wait for a reply + if( t_receive_reply->f_reply_consumer ) // this indicates that the reply queue was created, and we've started consuming on it; we should wait for a reply { LINFO( dlog, "Waiting for a reply from the server; use ctrl-c to cancel" ); // timed blocking call to wait for incoming message receiver t_msg_receiver; - core::post_listen_status t_post_listen_status = core::post_listen_status::unknown; auto t_rec_cancel_wrap = wrap_cancelable( t_msg_receiver ); scarab::signal_handler::add_cancelable( t_rec_cancel_wrap ); - dripline::reply_ptr_t t_reply = t_msg_receiver.wait_for_reply( t_receive_reply, t_post_listen_status, f_agent->get_timeout() ); + dripline::reply_ptr_t t_reply = t_msg_receiver.wait_for_reply( t_receive_reply, f_agent->get_timeout() ); if( t_msg_receiver.is_canceled() ) { @@ -266,23 +265,8 @@ namespace dripline } else { - if( t_post_listen_status == core::post_listen_status::timeout ) - { - LWARN( dlog, "Timed out or while waiting for reply" ); - f_agent->set_return( dl_client_error_timeout().rc_value() ); - } - else - { - if( t_post_listen_status == core::post_listen_status::hard_error ) - { - LERROR( dlog, "Error while waiting for reply" ); - } - else - { - LERROR( dlog, "Unknown state while waiting for reply: " << (int)t_post_listen_status ); - } - f_agent->set_return( dl_client_error().rc_value() ); - } + LWARN( dlog, "Timed out or error while waiting for reply" ); + f_agent->set_return( dl_client_error_timeout().rc_value() ); } f_agent->set_reply( t_reply ); } diff --git a/library/amqp.cc b/library/amqp.cc index 643f487e..570a466b 100644 --- a/library/amqp.cc +++ b/library/amqp.cc @@ -9,20 +9,25 @@ #include "amqp.hh" +#include "bdlde_base64encoder.h" +#include "bdlt_iso8601util.h" + +using namespace BloombergLP; + namespace dripline { - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::Table& a_table ) + DRIPLINE_API scarab::param_ptr_t table_to_param( const rmqt::FieldTable& a_table ) { scarab::param_ptr_t t_param( new scarab::param_node() ); scarab::param_node& t_node = (*t_param).as_node(); for( auto i_entry = a_table.begin(); i_entry != a_table.end(); ++i_entry ) { - t_node.add( i_entry->first, table_to_param( i_entry->second ) ); + t_node.add( std::string(i_entry->first), table_to_param( i_entry->second ) ); } return t_param; } - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::Array& a_array ) + DRIPLINE_API scarab::param_ptr_t table_to_param( const rmqt::FieldArray& a_array ) { scarab::param_ptr_t t_param( new scarab::param_array() ); scarab::param_array& t_array = (*t_param).as_array(); @@ -33,94 +38,104 @@ namespace dripline return t_param; } - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::TableValue& a_value ) + DRIPLINE_API scarab::param_ptr_t table_to_param( const rmqt::FieldValue& a_value ) { - using AmqpClient::TableValue; - - switch( a_value.GetType() ) + if( a_value.isUnset() ) + return scarab::param_ptr_t( new scarab::param() ); + if( a_value.is< bool >() ) + return scarab::param_ptr_t( new scarab::param_value( a_value.the< bool >() ) ); + if( a_value.is< int8_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (int64_t)a_value.the< int8_t >() ) ); + if( a_value.is< int16_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (int64_t)a_value.the< int16_t >() ) ); + if( a_value.is< int32_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (int64_t)a_value.the< int32_t >() ) ); + if( a_value.is< int64_t >() ) + return scarab::param_ptr_t( new scarab::param_value( a_value.the< int64_t >() ) ); + if( a_value.is< uint8_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (uint64_t)a_value.the< uint8_t >() ) ); + if( a_value.is< uint16_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (uint64_t)a_value.the< uint16_t >() ) ); + if( a_value.is< uint32_t >() ) + return scarab::param_ptr_t( new scarab::param_value( (uint64_t)a_value.the< uint32_t >() ) ); + if( a_value.is< float >() ) + return scarab::param_ptr_t( new scarab::param_value( (double)a_value.the< float >() ) ); + if( a_value.is< double >() ) + return scarab::param_ptr_t( new scarab::param_value( a_value.the< double >() ) ); + if( a_value.is< bsl::string >() ) + return scarab::param_ptr_t( new scarab::param_value( std::string( a_value.the< bsl::string >() ) ) ); + if( a_value.is< bsl::shared_ptr< rmqt::FieldArray > >() ) + { + const auto& t_array_ptr = a_value.the< bsl::shared_ptr< rmqt::FieldArray > >(); + if( ! t_array_ptr ) throw std::domain_error( "Null FieldArray pointer in rmqcpp FieldValue" ); + return table_to_param( *t_array_ptr ); + } + if( a_value.is< bsl::shared_ptr< rmqt::FieldTable > >() ) + { + const auto& t_table_ptr = a_value.the< bsl::shared_ptr< rmqt::FieldTable > >(); + if( ! t_table_ptr ) throw std::domain_error( "Null FieldTable pointer in rmqcpp FieldValue" ); + return table_to_param( *t_table_ptr ); + } + if( a_value.is< bsl::vector< bsl::uint8_t > >() ) + { + const bsl::vector< bsl::uint8_t >& t_bytes = a_value.the< bsl::vector< bsl::uint8_t > >(); + bdlde::Base64Encoder t_encoder( 0 ); // 0 maxLineLength = no line breaks + int t_max_len = bdlde::Base64Encoder::encodedLength( (int)t_bytes.size(), 0 ); + bsl::string t_b64( t_max_len, '\0' ); + int t_num_out = 0, t_num_in = 0; + t_encoder.convert( t_b64.begin(), &t_num_out, &t_num_in, + reinterpret_cast< const char* >( t_bytes.data() ), + reinterpret_cast< const char* >( t_bytes.data() + t_bytes.size() ) ); + int t_end_out = 0; + t_encoder.endConvert( t_b64.begin() + t_num_out, &t_end_out ); + t_b64.resize( t_num_out + t_end_out ); + return scarab::param_ptr_t( new scarab::param_value( std::string( t_b64 ) ) ); + } + if( a_value.is< bdlt::Datetime >() ) { - case TableValue::ValueType::VT_void: - return scarab::param_ptr_t( new scarab::param() ); - break; - case TableValue::ValueType::VT_bool: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetBool() ) ); - break; - case TableValue::ValueType::VT_int8: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetInt8() ) ); - break; - case TableValue::ValueType::VT_int16: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetInt16() ) ); - break; - case TableValue::ValueType::VT_int32: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetInt32() ) ); - break; - case TableValue::ValueType::VT_int64: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetInt64() ) ); - break; - case TableValue::ValueType::VT_float: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetFloat() ) ); - break; - case TableValue::ValueType::VT_double: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetDouble() ) ); - break; - case TableValue::ValueType::VT_string: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetString() ) ); - break; - case TableValue::ValueType::VT_array: - return table_to_param( a_value.GetArray() ); - break; - case TableValue::ValueType::VT_table: - return table_to_param( a_value.GetTable() ); - break; - case TableValue::ValueType::VT_uint8: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetUint8() ) ); - break; - case TableValue::ValueType::VT_uint16: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetUint16() ) ); - break; - case TableValue::ValueType::VT_uint32: - return scarab::param_ptr_t( new scarab::param_value( a_value.GetUint32() ) ); - break; - default: - throw std::domain_error( "Invalid SimpleAMQPClient TableValue type" ); + char t_buf[ 64 ]; + int t_len = bdlt::Iso8601Util::generate( t_buf, sizeof( t_buf ), a_value.the< bdlt::Datetime >() ); + if( t_len < 0 ) throw std::domain_error( "Failed to convert bdlt::Datetime to ISO 8601 string" ); + return scarab::param_ptr_t( new scarab::param_value( std::string( t_buf, t_len ) ) ); } - // should never get here + // uint64_t (deprecated in AMQP field tables) has no scarab equivalent + throw std::domain_error( "Unsupported rmqcpp FieldValue type" ); return scarab::param_ptr_t(); } - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_node& a_node ) + DRIPLINE_API rmqt::FieldValue param_to_table( const scarab::param_node& a_node ) { - AmqpClient::Table t_table; + auto t_table = bsl::make_shared< rmqt::FieldTable >(); for( auto i_entry = a_node.begin(); i_entry != a_node.end(); ++i_entry ) { - t_table.insert( AmqpClient::TableEntry( i_entry.name(), param_to_table(*i_entry) ) ); + (*t_table)[ bsl::string( i_entry.name() ) ] = param_to_table( *i_entry ); } - return t_table; + return rmqt::FieldValue( t_table ); } - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_array& a_array ) + DRIPLINE_API rmqt::FieldValue param_to_table( const scarab::param_array& a_array ) { - AmqpClient::Array t_array; + auto t_array = bsl::make_shared< rmqt::FieldArray >(); for( auto i_entry = a_array.begin(); i_entry != a_array.end(); ++i_entry ) { - t_array.push_back( param_to_table( *i_entry ) ); + t_array->push_back( param_to_table( *i_entry ) ); } - return t_array; + return rmqt::FieldValue( t_array ); } - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_value& a_value ) + DRIPLINE_API rmqt::FieldValue param_to_table( const scarab::param_value& a_value ) { - if( a_value.is_bool() ) return AmqpClient::TableValue( a_value.as_bool() ); - if( a_value.is_int() ) return AmqpClient::TableValue( a_value.as_int() ); - if( a_value.is_uint() ) return AmqpClient::TableValue( (uint32_t)a_value.as_uint() ); - if( a_value.is_double() ) return AmqpClient::TableValue( a_value.as_double() ); - if( a_value.is_string() ) return AmqpClient::TableValue( a_value.as_string() ); + if( a_value.is_bool() ) return rmqt::FieldValue( a_value.as_bool() ); + if( a_value.is_int() ) return rmqt::FieldValue( (int64_t)a_value.as_int() ); + if( a_value.is_uint() ) return rmqt::FieldValue( (uint32_t)a_value.as_uint() ); + if( a_value.is_double() ) return rmqt::FieldValue( a_value.as_double() ); + if( a_value.is_string() ) return rmqt::FieldValue( bsl::string( a_value.as_string() ) ); throw std::domain_error( "Invalid param value type" ); } - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param& a_param ) + DRIPLINE_API rmqt::FieldValue param_to_table( const scarab::param& a_param ) { - if( a_param.is_null() ) return AmqpClient::TableValue(); + if( a_param.is_null() ) return rmqt::FieldValue(); if( a_param.is_node() ) return param_to_table( a_param.as_node() ); if( a_param.is_array() ) return param_to_table( a_param.as_array() ); if( a_param.is_value() ) return param_to_table( a_param.as_value() ); diff --git a/library/amqp.hh b/library/amqp.hh index 8ba2d178..ed4c371e 100644 --- a/library/amqp.hh +++ b/library/amqp.hh @@ -12,33 +12,29 @@ #include "param.hh" -#include "SimpleAmqpClient/SimpleAmqpClient.h" - -#include "SimpleAmqpClient/AmqpException.h" -#include "SimpleAmqpClient/AmqpLibraryException.h" +#include "rmqt_fieldvalue.h" +#include "rmqt_message.h" +#include "rmqp_messageguard.h" +#include "bsl_memory.h" namespace dripline { // convenience typedefs - typedef AmqpClient::Channel::ptr_t amqp_channel_ptr; - typedef AmqpClient::Envelope::ptr_t amqp_envelope_ptr; - typedef AmqpClient::BasicMessage::ptr_t amqp_message_ptr; - - typedef AmqpClient::AmqpException amqp_exception; - typedef AmqpClient::AmqpLibraryException amqp_lib_exception; + typedef BloombergLP::rmqp::TransferrableMessageGuard amqp_envelope_ptr; + typedef bsl::shared_ptr< BloombergLP::rmqt::Message > amqp_message_ptr; typedef std::vector< amqp_message_ptr > amqp_split_message_ptrs; // conversion functions - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::Table& a_table ); - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::Array& a_array ); - DRIPLINE_API scarab::param_ptr_t table_to_param( const AmqpClient::TableValue& a_value ); - - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_node& a_node ); - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_array& a_array ); - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param_value& a_value ); - DRIPLINE_API AmqpClient::TableValue param_to_table( const scarab::param& a_value ); + DRIPLINE_API scarab::param_ptr_t table_to_param( const BloombergLP::rmqt::FieldTable& a_table ); + DRIPLINE_API scarab::param_ptr_t table_to_param( const BloombergLP::rmqt::FieldArray& a_array ); + DRIPLINE_API scarab::param_ptr_t table_to_param( const BloombergLP::rmqt::FieldValue& a_value ); + + DRIPLINE_API BloombergLP::rmqt::FieldValue param_to_table( const scarab::param_node& a_node ); + DRIPLINE_API BloombergLP::rmqt::FieldValue param_to_table( const scarab::param_array& a_array ); + DRIPLINE_API BloombergLP::rmqt::FieldValue param_to_table( const scarab::param_value& a_value ); + DRIPLINE_API BloombergLP::rmqt::FieldValue param_to_table( const scarab::param& a_value ); } /* namespace dripline */ diff --git a/library/core.cc b/library/core.cc index 3e04b3f4..38631929 100644 --- a/library/core.cc +++ b/library/core.cc @@ -12,6 +12,16 @@ #include "dripline_exceptions.hh" #include "message.hh" +#include "rmqa_consumer.h" +#include "rmqa_rabbitcontext.h" +#include "rmqa_vhost.h" +#include "rmqa_producer.h" +#include "rmqp_messageguard.h" +#include "rmqt_queue.h" +#include "rmqt_simpleendpoint.h" +#include "rmqt_plaincredentials.h" +#include "rmqt_properties.h" + #include "authentication.hh" #include "exponential_backoff.hh" #include "logger.hh" @@ -28,20 +38,13 @@ namespace dripline sent_msg_pkg::~sent_msg_pkg() { - if( f_channel ) + if( f_reply_consumer ) { - try - { - LDEBUG( dlog, "Stopping consuming messages" ); - f_channel->BasicCancel( f_consumer_tag ); - } - catch( amqp_exception& e ) + LDEBUG( dlog, "Canceling reply consumer" ); + auto t_result = f_reply_consumer->cancelAndDrain(); + if( ! t_result ) { - LERROR( dlog, "AMQP exception caught while canceling the channel: (" << e.reply_code() << ") " << e.reply_text() ); - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP library exception caught while canceling the channel: (" << e.ErrorCode() << ") " << e.what() ); + LWARN( dlog, "Error while canceling reply consumer: " << t_result.error() ); } } } @@ -53,12 +56,13 @@ namespace dripline f_port(), f_username(), f_password(), - f_requests_exchange(), - f_alerts_exchange(), f_heartbeat_routing_key(), f_max_payload_size(), f_make_connection(), - f_max_connection_attempts() + f_max_connection_attempts(), + f_rabbit_context(), + f_vhost(), + f_connection_mutex( std::make_shared< std::mutex >() ) { // Get the default values, and merge in the supplied a_config // a_config's default value is also dripline_config, but the user can supply an arbitrary node. @@ -84,14 +88,14 @@ namespace dripline f_password = t_auth.get( t_auth_group, "password", f_password ); */ // Replace local parameters with values from the config - f_address = t_config["broker"]().as_string(); //.get_value("broker", "localhost"); - f_port = t_config["broker_port"]().as_uint(); //.get_value("broker_port", 5672); - f_requests_exchange = t_config["requests_exchange"]().as_string(); //.get_value("requests_exchange", "requests"); - f_alerts_exchange = t_config["alerts_exchange"]().as_string(); //.get_value("alerts_exchange", "alerts"); - f_heartbeat_routing_key = t_config["heartbeat_routing_key"]().as_string(); //.get_value("heartbeat_routing_key", "heartbeat"); + f_address = t_config["broker"]().as_string(); + f_port = t_config["broker_port"]().as_uint(); + f_requests_ex.f_name = t_config["requests_exchange"]().as_string(); + f_alerts_ex.f_name = t_config["alerts_exchange"]().as_string(); + f_heartbeat_routing_key = t_config["heartbeat_routing_key"]().as_string(); f_make_connection = t_config.get_value( "make_connection", a_make_connection ); - f_max_payload_size = t_config["max_payload_size"]().as_uint(); //.get_value("max_payload_size", DL_MAX_PAYLOAD_SIZE); - f_max_connection_attempts = t_config["max_connection_attempts"]().as_uint(); //.get_value("max_connection_attempts", 10); + f_max_payload_size = t_config["max_payload_size"]().as_uint(); + f_max_connection_attempts = t_config["max_connection_attempts"]().as_uint(); f_username = a_auth.get("dripline", "username", "guest"); f_password = a_auth.get("dripline", "password", "guest"); @@ -149,451 +153,386 @@ namespace dripline } } - sent_msg_pkg_ptr core::send( request_ptr_t a_request, amqp_channel_ptr a_channel ) const + sent_msg_pkg_ptr core::send( request_ptr_t a_request ) const { LDEBUG( dlog, "Sending request with routing key <" << a_request->routing_key() << ">" ); if ( ! f_make_connection || core::s_offline ) { throw a_request; - //throw dripline_error() << "cannot send reply when make_connection is false"; } - return do_send( std::static_pointer_cast< message >( a_request ), f_requests_exchange, true, a_channel ); + return do_send( std::static_pointer_cast< message >( a_request ), f_requests_ex.f_name, true ); } - sent_msg_pkg_ptr core::send( reply_ptr_t a_reply, amqp_channel_ptr a_channel ) const + sent_msg_pkg_ptr core::send( reply_ptr_t a_reply ) const { LDEBUG( dlog, "Sending reply with routing key <" << a_reply->routing_key() << ">" ); if ( ! f_make_connection || core::s_offline ) { throw a_reply; - //throw dripline_error() << "cannot send reply when make_connection is false"; } - return do_send( std::static_pointer_cast< message >( a_reply ), f_requests_exchange, false, a_channel ); + return do_send( std::static_pointer_cast< message >( a_reply ), f_requests_ex.f_name, false ); } - sent_msg_pkg_ptr core::send( alert_ptr_t a_alert, amqp_channel_ptr a_channel ) const + sent_msg_pkg_ptr core::send( alert_ptr_t a_alert ) const { LDEBUG( dlog, "Sending alert with routing key <" << a_alert->routing_key() << ">" ); if ( ! f_make_connection || core::s_offline ) { throw a_alert; - //throw dripline_error() << "cannot send reply when make_connection is false"; } - return do_send( std::static_pointer_cast< message >( a_alert ), f_alerts_exchange, false, a_channel ); + return do_send( std::static_pointer_cast< message >( a_alert ), f_alerts_ex.f_name, false ); } - sent_msg_pkg_ptr core::do_send( message_ptr_t a_message, const std::string& a_exchange, bool a_expect_reply, amqp_channel_ptr a_channel ) const + sent_msg_pkg_ptr core::do_send( message_ptr_t a_message, const std::string& a_exchange, bool a_expect_reply ) const { // throws connection_error if it could not connect with the broker - // throws dripline_error if there's a problem with the exchange or creating the AMQP message object(s) - // returns the receive_reply package if the message was completely or partially sent - // the f_successful_send flag will be set accordingly: true if completely sent; false if partially sent - // if there was an error sending the message, that will be returned in f_send_error_message, which will be empty otherwise + // throws dripline_error if there's a problem creating the AMQP message object(s) + // returns the sent_msg_pkg; f_successful_send indicates whether the send succeeded - // lambda to create a string with the basic information about the send attempt auto t_diagnostic_string_maker = [a_message, this]() -> std::string { - return std::string("Broker: ") + f_address +"\nPort: " + std::to_string(f_port) + "\nRouting Key: " + a_message->routing_key(); + return std::string("Broker: ") + f_address + "\nPort: " + std::to_string(f_port) + "\nRouting Key: " + a_message->routing_key(); }; - amqp_channel_ptr t_channel = a_channel ? a_channel : open_channel(); - if( ! t_channel ) - { - throw connection_error() << "Unable to open channel to send message\n" << t_diagnostic_string_maker(); - } + open_connection(); - if( ! setup_exchange( t_channel, a_exchange ) ) + if( ! f_vhost ) { - throw dripline_error() << "Unable to setup the exchange <" << a_exchange << "> to send message\n" << t_diagnostic_string_maker(); + throw connection_error() << "Not connected to broker\n" << t_diagnostic_string_maker(); } - // create empty receive-reply object sent_msg_pkg_ptr t_receive_reply = std::make_shared< sent_msg_pkg >(); - std::unique_lock< std::mutex > t_rr_lock( t_receive_reply->f_mutex ); - - if( a_expect_reply ) - { - t_receive_reply->f_channel = t_channel; - - // create the reply-to queue, and bind the queue to the routing key over the given exchange - std::string t_reply_to = t_channel->DeclareQueue( "" ); - t_channel->BindQueue( t_reply_to, a_exchange, t_reply_to ); - // set the reply-to in the message because now we have the queue to which to reply - a_message->reply_to() = t_reply_to; - - // begin consuming on the reply-to queue - t_receive_reply->f_consumer_tag = t_channel->BasicConsume( t_reply_to ); - LDEBUG( dlog, "Reply-to for request: " << t_reply_to ); - LDEBUG( dlog, "Consumer tag for reply: " << t_receive_reply->f_consumer_tag ); - } - - // convert the dripline::message object to an AMQP message - amqp_split_message_ptrs t_amqp_messages = a_message->create_amqp_messages( f_max_payload_size ); - if( t_amqp_messages.empty() ) - { - throw dripline_error() << "Unable to convert the dripline::message object to AMQP message(s) to be sent\n" << t_diagnostic_string_maker(); - } try { - LDEBUG( dlog, "Sending message to <" << a_message->routing_key() << ">" ); - for( amqp_message_ptr& t_amqp_message : t_amqp_messages ) + if( a_expect_reply ) + { + send_withreply( a_message, a_exchange, t_receive_reply ); + } + else { - // send the message - // the first boolean argument is whether it's mandatory that the message be delivered to a queue. - // this is only the case for requests, where we expect something to be listening. - t_channel->BasicPublish( a_exchange, a_message->routing_key(), t_amqp_message, a_message->is_request(), false ); + if( ! send_noreply( a_message, a_exchange ) ) + { + t_receive_reply->f_successful_send = false; + t_receive_reply->f_send_error_message = "Error in send_noreply\n" + t_diagnostic_string_maker(); + return t_receive_reply; + } } - LDEBUG( dlog, "Message sent in " << t_amqp_messages.size() << " chunks" ); + LDEBUG( dlog, "Message sent to <" << a_message->routing_key() << ">" ); t_receive_reply->f_successful_send = true; t_receive_reply->f_send_error_message.clear(); } - catch( AmqpClient::ConnectionClosedException& e ) - { - LERROR( dlog, "Unable to send message because the connection is closed: " << e.what() ); - throw connection_error() << "Unable to send message because the connection is closed: " << e.what() << '\n' << t_diagnostic_string_maker(); - } - catch( AmqpClient::AmqpLibraryException& e ) - { - LERROR( dlog, "AMQP error while sending message: " << e.what() ); - t_receive_reply->f_successful_send = false; - t_receive_reply->f_send_error_message = std::string("AMQP error while sending message: ") + std::string(e.what()) + '\n' + t_diagnostic_string_maker(); - } - catch( AmqpClient::MessageReturnedException& e ) + catch( connection_error& ) { - LERROR( dlog, "Message was returned: " << e.what() ); - t_receive_reply->f_successful_send = false; - t_receive_reply->f_send_error_message = std::string("Message was returned: ") + std::string(e.what()) + '\n' + t_diagnostic_string_maker(); + throw; } catch( std::exception& e ) { LERROR( dlog, "Error while sending message: " << e.what() ); t_receive_reply->f_successful_send = false; - t_receive_reply->f_send_error_message = std::string("Error while sending message: ") + std::string(e.what()) + '\n' + t_diagnostic_string_maker(); + t_receive_reply->f_send_error_message = std::string("Error while sending message: ") + e.what() + '\n' + t_diagnostic_string_maker(); } return t_receive_reply; } - amqp_channel_ptr core::open_channel() const + void core::open_connection() const { - // Exceptions that can be encountered while opening a channel - // SimpleAmqpClient::Channel::Open(opts) - // std::runtime_error -- options are invalid; auth not specified - // std::logic_error -- unhandled auth type - // std::bad_alloc -- connection is null - // amqp_exception -- unsure of what would cause this - // amqp_lib_exception -- unable to make connection to the broker; maybe other things + std::lock_guard< std::mutex > t_lock( *f_connection_mutex ); - if ( ! f_make_connection || core::s_offline ) + if( f_vhost ) { - return amqp_channel_ptr(); - //throw dripline_error() << "Should not call open_channel when offline"; + return; // already connected + } + if( ! f_make_connection || s_offline ) + { + return; } - amqp_channel_ptr t_ret_ptr = amqp_channel_ptr(); + LINFO( dlog, "Opening AMQP connection to " << f_address << ":" << f_port ); + LDEBUG( dlog, "Using broker authentication: " << f_username << ":" << f_password ); - auto t_open_conn_fcn = [&]()->bool - { - try - { - LINFO( dlog, "Opening AMQP connection and creating channel to " << f_address << ":" << f_port ); - LDEBUG( dlog, "Using broker authentication: " << f_username << ":" << f_password ); - struct AmqpClient::Channel::OpenOpts opts; - opts.host = f_address; - opts.port = f_port; - opts.auth = AmqpClient::Channel::OpenOpts::BasicAuth(f_username, f_password); - t_ret_ptr = AmqpClient::Channel::Open( opts ); - return true; - } - catch( amqp_exception& e ) - { - if( e.is_soft_error() ) - { - LWARN( dlog, "Recoverable AMQP exception caught while opening channel: (" << e.reply_code() << ") " << e.reply_text() ); - return false; - } - // otherwise error is non-recoverable - throw; - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP Library Exception caught while creating channel: (" << e.ErrorCode() << ") " << e.what() ); - if( e.ErrorCode() == -9 ) - { - LERROR( dlog, "This error means the client could not connect to the broker.\n" << - "Check that you have the address and port correct, and that the broker is running.") - } - return false; - } - // std::exceptions are non-recoverable, so don't catch them - }; + using namespace BloombergLP; - scarab::exponential_backoff<> t_open_conn_backoff( t_open_conn_fcn, f_max_connection_attempts ); - auto t_exp_cancel_wrap = wrap_cancelable( t_open_conn_backoff ); - scarab::signal_handler::add_cancelable( t_exp_cancel_wrap ); + // Set any options we need in the RabbitContext + rmqa::RabbitContextOptions t_options; + using namespace bsls::TimeIntervalLiterals; + t_options.setConnectionErrorThreshold( 10_s ); // TODO: make this appropriately configurable via dripline_config - int t_expback_return = 0; - try - { - LDEBUG( dlog, "Attempting to open channel; will make up to " << f_max_connection_attempts << " attempts" ); - t_expback_return = t_open_conn_backoff.go(); - // either succeeded or failed after multiple attempts - } - catch( amqp_exception& e ) - { - LERROR( dlog, "Unrecoverable AMQP exception caught while opening channel: (" << e.reply_code() << ") " << e.reply_text() ); - } - catch(const std::exception& e) - { - // unrecoverable error causing a std::exception - LERROR( dlog, "Standard exception caught while creating channel: " << e.what() ); - } + f_rabbit_context = bsl::make_shared< rmqa::RabbitContext >( t_options ); - if( t_expback_return == 0 ) - { - LERROR( dlog, "Failed to open a channel; no more attempts will be made" ); - } - - return t_ret_ptr; - } + auto t_endpoint = bsl::make_shared< rmqt::SimpleEndpoint >( f_address, "/", (bsl::uint16_t)f_port ); + auto t_credentials = bsl::make_shared< rmqt::PlainCredentials >( f_username, f_password ); + f_vhost = f_rabbit_context->createVHostConnection( "dripline", t_endpoint, t_credentials ); - bool core::setup_exchange( amqp_channel_ptr a_channel, const std::string& a_exchange ) - { - if( s_offline || ! a_channel ) + if( ! f_vhost ) { - return false; + f_rabbit_context.reset(); + throw connection_error() << "Unable to create vhost connection to " << f_address << ":" << f_port; } - try - { - LDEBUG( dlog, "Declaring exchange <" << a_exchange << ">" ); - a_channel->DeclareExchange( a_exchange, AmqpClient::Channel::EXCHANGE_TYPE_TOPIC, false, false, false ); - return true; - } - catch( amqp_exception& e ) + // Declare both exchanges + f_requests_ex.create_exchange(); + f_alerts_ex.create_exchange(); + //f_requests_ex.f_exchange = f_topology.addExchange( bsl::string(f_requests_ex.f_name), rmqt::ExchangeType::TOPIC, rmqt::AutoDelete::OFF, rmqt::Durable::ON, rmqt::Internal::NO ); + //f_alerts_ex.f_exchange = f_topology.addExchange( bsl::string(f_alerts_ex.f_name), rmqt::ExchangeType::TOPIC, rmqt::AutoDelete::OFF, rmqt::Durable::ON, rmqt::Internal::NO ); + + // Create requests producer { - LERROR( dlog, "AMQP exception caught while declaring exchange: (" << e.reply_code() << ") " << e.reply_text() ); - return false; + auto t_result = f_vhost->createProducer( f_requests_ex.f_topology, f_requests_ex.f_exchange, 10 ); + if( ! t_result ) + { + f_vhost.reset(); + f_rabbit_context.reset(); + throw connection_error() << "Unable to create requests producer: " << t_result.error(); + } + f_requests_ex.f_producer = t_result.value(); } - catch( amqp_lib_exception& e ) + + // Create alerts producer { - LERROR( dlog, "AMQP library exception caught while declaring exchange: (" << e.ErrorCode() << ") " << e.what() ); - return false; + auto t_result = f_vhost->createProducer( f_alerts_ex.f_topology, f_alerts_ex.f_exchange, 10 ); + if( ! t_result ) + { + f_requests_ex.f_producer.reset(); + f_vhost.reset(); + f_rabbit_context.reset(); + throw connection_error() << "Unable to create alerts producer: " << t_result.error(); + } + f_alerts_ex.f_producer = t_result.value(); } + + LINFO( dlog, "AMQP connection established" ); } - bool core::setup_queue( amqp_channel_ptr a_channel, const std::string& a_queue_name ) + BloombergLP::rmqt::QueueHandle core::add_requests_queue( const std::string& a_queue_name, + bool a_auto_delete, bool a_durable, + const scarab::param_node& a_field_table ) { - if( s_offline || ! a_channel ) - { - return false; - } + return f_requests_ex.add_queue( a_queue_name, a_auto_delete, a_durable, a_field_table ); + } - try - { - LDEBUG( dlog, "Declaring queue <" << a_queue_name << ">" ); - a_channel->DeclareQueue( a_queue_name, false, false, true, true ); - return true; - } - catch( amqp_exception& e ) - { - LERROR( dlog, "AMQP exception caught while declaring queue: (" << e.reply_code() << ") " << e.reply_text() ); - return false; - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP library exception caught while declaring queue: (" << e.ErrorCode() << ") " << e.what() ); - return false; - } +// BloombergLP::rmqt::QueueHandle core::add_requests_ephemeral_queue( const std::string& a_queue_name ) +// { +// return f_requests_ex.add_ephemeral_queue( f_topology, a_queue_name ); +// } + void core::bind_requests_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ) + { + f_requests_ex.bind_key( a_queue_name, a_routing_key, a_queue ); } - bool core::bind_key( amqp_channel_ptr a_channel, const std::string& a_exchange, const std::string& a_queue_name, const std::string& a_routing_key ) + BloombergLP::rmqt::QueueHandle core::add_alerts_queue( const std::string& a_queue_name, + bool a_auto_delete, bool a_durable, + const scarab::param_node& a_field_table ) { - if( s_offline || ! a_channel ) - { - return false; - } + return f_alerts_ex.add_queue( a_queue_name, a_auto_delete, a_durable, a_field_table ); + } - try - { - LDEBUG( dlog, "Binding key <" << a_routing_key << "> to queue <" << a_queue_name << "> over exchange <" << a_exchange << ">" ); - a_channel->BindQueue( a_queue_name, a_exchange, a_routing_key ); +// BloombergLP::rmqt::QueueHandle core::add_alerts_ephemeral_queue( const std::string& a_queue_name ) +// { +// return f_alerts_ex.add_ephemeral_queue( f_topology, a_queue_name ); +// } - return true; - } - catch( amqp_exception& e ) - { - LERROR( dlog, "AMQP exception caught while declaring binding key <" << a_routing_key << ">: (" << e.reply_code() << ") " << e.reply_text() ); - return false; - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP library exception caught while binding key <" << a_routing_key << ">: (" << e.ErrorCode() << ") " << e.what() ); - return false; - } + void core::bind_alerts_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ) + { + f_alerts_ex.bind_key( a_queue_name, a_routing_key, a_queue ); + } + + //*************************** + // Exchange store definitions + //*************************** + + void core::exchange_store::create_exchange() + { + using namespace BloombergLP; + f_exchange = f_topology.addExchange( bsl::string(f_name), rmqt::ExchangeType::TOPIC, rmqt::AutoDelete::OFF, rmqt::Durable::ON, rmqt::Internal::NO ); + return; } - std::string core::start_consuming( amqp_channel_ptr a_channel, const std::string& a_queue_name ) + BloombergLP::rmqt::QueueHandle core::exchange_store::add_queue( const std::string& a_queue_name, + bool a_auto_delete, bool a_durable, + const scarab::param_node& a_field_table ) { - if( s_offline || ! a_channel ) + using namespace BloombergLP; + LDEBUG( dlog, "Declaring durable queue <" << a_queue_name << "> on exchange <" << f_name << ">" ); + bsl::shared_ptr t_bsl_field_table = param_to_table(a_field_table).the< bsl::shared_ptr >(); + rmqt::QueueHandle t_queue = f_topology.addQueue( bsl::string(a_queue_name), + rmqt::AutoDelete::Value(int(a_auto_delete)), rmqt::Durable::Value(int(a_durable)), + *t_bsl_field_table ); + if( ! t_queue.lock() ) { - return std::string(); + throw dripline_error() << "Queue could not be created. See log for error message."; } + return t_queue; + } - try - { - LDEBUG( dlog, "Starting to consume messages on queue <" << a_queue_name << ">" ); - // second bool is setting no_ack to false - return a_channel->BasicConsume( a_queue_name, "", true, false ); - } - catch( amqp_exception& e ) - { - LERROR( dlog, "AMQP exception caught while starting consuming messages on <" << a_queue_name << ">: (" << e.reply_code() << ") " << e.reply_text() ); - return std::string(); - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP library exception caught while starting consuming messages on <" << a_queue_name << ">: (" << e.ErrorCode() << ") " << e.what() ); - return std::string(); - } +// BloombergLP::rmqt::QueueHandle core::exchange_store::add_ephemeral_queue( BloombergLP::rmqa::Topology& a_topo, const std::string& a_queue_name ) +// { +// using namespace BloombergLP; +// LDEBUG( dlog, "Declaring ephemeral queue <" << a_queue_name << "> on exchange <" << f_name << ">" ); +// return a_topo.addQueue( bsl::string(a_queue_name), rmqt::AutoDelete::ON, rmqt::Durable::ON ); +// } + + void core::exchange_store::bind_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ) + { + LDEBUG( dlog, "Binding queue <" << a_queue_name << "> to exchange <" << f_name << "> with routing key <" << a_routing_key << ">" ); + f_topology.bind( f_exchange, + a_queue, + bsl::string(a_routing_key) ); } - bool core::stop_consuming( amqp_channel_ptr a_channel, std::string& a_consumer_tag ) + //*************************** + //*************************** + + void core::send_withreply( message_ptr_t a_message, const std::string& a_exchange, sent_msg_pkg_ptr a_pkg ) const { - if( s_offline || ! a_channel ) - { - return false; - } + using namespace BloombergLP; + + // Generate a unique name for the temporary reply queue + std::string t_reply_to = string_from_uuid( generate_random_uuid() ); + a_message->reply_to() = t_reply_to; + LDEBUG( dlog, "Reply-to for request: " << t_reply_to ); + + // Build a local topology for the temporary reply queue only. + // This topology is separate from f_topology so that the transient queue does not + // pollute the persistent topology passed to message_dispatcher::start_listening(). + rmqa::Topology t_reply_topo; + auto t_ex = t_reply_topo.addExchange( a_exchange, rmqt::ExchangeType::TOPIC, rmqt::AutoDelete::OFF, rmqt::Durable::ON, rmqt::Internal::NO ); + auto t_queue = t_reply_topo.addQueue( t_reply_to, rmqt::AutoDelete::ON, rmqt::Durable::OFF ); + t_reply_topo.bind( t_ex, t_queue, t_reply_to ); + + // Create promise; the consumer callback will fulfil it when the complete reply is assembled + a_pkg->f_reply_promise = std::make_shared< std::promise< reply_ptr_t > >(); + auto t_promise = a_pkg->f_reply_promise; + + // Inline accumulator struct for multi-chunk replies + struct reply_pack + { + amqp_split_message_ptrs f_messages; + unsigned f_chunks_received; + std::string f_routing_key; + std::mutex f_mutex; + reply_pack() : f_messages(), f_chunks_received( 0 ), f_routing_key(), f_mutex() {} + }; + auto t_pack = std::make_shared< reply_pack >(); - try - { - LDEBUG( dlog, "Stopping consuming messages for consumer <" << a_consumer_tag << ">" ); - a_channel->BasicCancel( a_consumer_tag ); - a_consumer_tag.clear(); - return true; - } - catch( amqp_exception& e ) + auto t_callback = [t_pack, t_promise]( rmqp::MessageGuard& guard ) { - LERROR( dlog, "AMQP exception caught while stopping consuming messages on <" << a_consumer_tag << ">: (" << e.reply_code() << ") " << e.reply_text() ); - return false; - } - catch( amqp_lib_exception& e ) + auto t_amqp_message = bsl::make_shared< rmqt::Message >( guard.message() ); + const std::string t_msg_id( t_amqp_message->messageId() ); + auto t_parsed_id = message::parse_message_id( t_msg_id ); + unsigned t_chunk_idx = std::get< 1 >( t_parsed_id ); + unsigned t_total_chunks = std::get< 2 >( t_parsed_id ); + + bool t_complete = false; + { + std::lock_guard< std::mutex > t_lock( t_pack->f_mutex ); + if( t_pack->f_messages.empty() ) + { + t_pack->f_messages.resize( t_total_chunks ); + t_pack->f_routing_key = std::string( guard.envelope().routingKey() ); + } + t_pack->f_messages[t_chunk_idx] = t_amqp_message; + ++t_pack->f_chunks_received; + t_complete = ( t_pack->f_chunks_received == t_pack->f_messages.size() ); + } + + guard.ack(); + + if( t_complete ) + { + try + { + message_ptr_t t_message = message::process_message( t_pack->f_messages, t_pack->f_routing_key ); + reply_ptr_t t_reply = std::dynamic_pointer_cast< msg_reply >( t_message ); + if( t_reply ) + { + t_promise->set_value( t_reply ); + } + else + { + t_promise->set_exception( std::make_exception_ptr( + dripline_error() << "Expected reply but received a different message type" ) ); + } + } + catch( ... ) + { + t_promise->set_exception( std::current_exception() ); + } + } + }; + + // Create the reply consumer on the temporary queue + auto t_consumer_result = f_vhost->createConsumer( t_reply_topo, t_queue, t_callback, "reply-" + t_reply_to, 1 ); + if( ! t_consumer_result ) { - LERROR( dlog, "AMQP library exception caught while stopping consuming messages on <" << a_consumer_tag << ">: (" << e.ErrorCode() << ") " << e.what() ); - return false; + throw connection_error() << "Unable to create reply consumer: " << t_consumer_result.error(); } - catch( AmqpClient::ConsumerTagNotFoundException& e ) + a_pkg->f_reply_consumer = t_consumer_result.value(); + + // Send message chunks via the requests producer + amqp_split_message_ptrs t_amqp_messages = a_message->create_amqp_messages( f_max_payload_size ); + if( t_amqp_messages.empty() ) { - LERROR( dlog, "Fatal AMQP exception encountered while stopping consuming messages on <" << a_consumer_tag << ">: " << e.what() ); - return false; + throw dripline_error() << "Unable to convert dripline::message to AMQP message(s)"; } - catch( std::exception& e ) + + LDEBUG( dlog, "Sending request to <" << a_message->routing_key() << "> in " << t_amqp_messages.size() << " chunk(s)" ); + for( amqp_message_ptr& t_amqp_message : t_amqp_messages ) { - LERROR( dlog, "Standard exception caught while stopping consuming messages on <" << a_consumer_tag << ">: " << e.what() ); - return false; + auto t_status = f_requests_ex.f_producer->send( + *t_amqp_message, + a_message->routing_key(), + []( const rmqt::Message&, const bsl::string&, const rmqt::ConfirmResponse& ) {} ); + if( t_status != rmqp::Producer::SENDING ) + { + throw dripline_error() << "Failed to enqueue request chunk for sending; status=" << t_status; + } } } - bool core::remove_queue( amqp_channel_ptr a_channel, const std::string& a_queue_name ) + bool core::send_noreply( message_ptr_t a_message, const std::string& a_exchange ) const { - if( s_offline || ! a_channel ) - { - return false; - } + using namespace BloombergLP; - try - { - LDEBUG( dlog, "Deleting queue <" << a_queue_name << ">" ); - a_channel->DeleteQueue( a_queue_name, false ); - return true; - } - catch( AmqpClient::ConnectionClosedException& e ) + bsl::shared_ptr< rmqa::Producer > t_producer; + if( a_exchange == f_alerts_ex.f_name ) { - LERROR( dlog, "Fatal AMQP exception encountered removing queue <" << a_queue_name << ">: " << e.what() ); - return false; + t_producer = f_alerts_ex.f_producer; } - catch( amqp_lib_exception& e ) + else { - LERROR( dlog, "AMQP library exception caught while removing queue <" << a_queue_name << ">: (" << e.ErrorCode() << ") " << e.what() ); - return false; + t_producer = f_requests_ex.f_producer; } - catch( std::exception& e ) + + if( ! t_producer ) { - LERROR( dlog, "Standard exception caught while removing queue <" << a_queue_name << ">: " << e.what() ); + LERROR( dlog, "No producer available for exchange <" << a_exchange << ">" ); return false; } - } - void core::listen_for_message( amqp_envelope_ptr& a_envelope, core::post_listen_status& a_status, amqp_channel_ptr a_channel, const std::string& a_consumer_tag, int a_timeout_ms, bool a_do_ack ) - { - if( s_offline || ! a_channel ) + amqp_split_message_ptrs t_amqp_messages = a_message->create_amqp_messages( f_max_payload_size ); + if( t_amqp_messages.empty() ) { - a_status = core::post_listen_status::unknown; - return; + LERROR( dlog, "Unable to convert dripline::message to AMQP message(s)" ); + return false; } - while( true ) + LDEBUG( dlog, "Sending message to <" << a_message->routing_key() << "> in " << t_amqp_messages.size() << " chunk(s)" ); + bool t_all_sent = true; + for( amqp_message_ptr& t_amqp_message : t_amqp_messages ) { - try + auto t_status = t_producer->send( + *t_amqp_message, + a_message->routing_key(), + []( const rmqt::Message&, const bsl::string&, const rmqt::ConfirmResponse& ) {} ); + if( t_status != rmqp::Producer::SENDING ) { - if( a_timeout_ms > 0 ) - { - a_channel->BasicConsumeMessage( a_consumer_tag, a_envelope, a_timeout_ms ); - } - else - { - a_envelope = a_channel->BasicConsumeMessage( a_consumer_tag ); - } - if( a_envelope ) - { - if( a_do_ack ) a_channel->BasicAck( a_envelope ); - a_status = post_listen_status::message_received; - } - else - { - a_status = post_listen_status::timeout; - } - return; - } - catch( AmqpClient::ConnectionClosedException& e ) - { - LERROR( dlog, "Fatal AMQP exception encountered: " << e.what() ); - a_status = post_listen_status::hard_error; - return; - } - catch( AmqpClient::ConsumerCancelledException& e ) - { - LERROR( dlog, "Fatal AMQP exception encountered: " << e.what() ); - a_status = post_listen_status::hard_error; - return; - } - catch( AmqpClient::AmqpException& e ) - { - if( e.is_soft_error() ) - { - LWARN( dlog, "Non-fatal AMQP exception encountered: " << e.reply_text() ); - a_status = post_listen_status::soft_error; - return; - } - LERROR( dlog, "Fatal AMQP exception encountered: " << e.reply_text() ); - a_status = post_listen_status::hard_error; - return; - } - catch( std::exception& e ) - { - LERROR( dlog, "Standard exception caught: " << e.what() ); - a_status = post_listen_status::hard_error; - return; - } - catch(...) - { - LERROR( dlog, "Unknown exception caught" ); - a_status = post_listen_status::hard_error; - return; + LERROR( dlog, "Failed to enqueue message chunk for sending; status=" << t_status ); + t_all_sent = false; } } + return t_all_sent; } } /* namespace dripline */ - diff --git a/library/core.hh b/library/core.hh index d8ca0e85..9f7edd56 100644 --- a/library/core.hh +++ b/library/core.hh @@ -11,8 +11,15 @@ #include "dripline_config.hh" #include "message.hh" -#include +#include "param_helpers_impl.hh" + +#include "rmqa_topology.h" +#include "rmqt_exchange.h" + +#include +#include #include +#include #include namespace scarab @@ -21,26 +28,45 @@ namespace scarab class param_node; } +namespace BloombergLP { + namespace rmqa { class Consumer; class RabbitContext; class VHost; class Producer; } + namespace rmqt + { + class Queue; + typedef bsl::weak_ptr QueueHandle; + } +} + namespace dripline { /*! @class sent_msg_pkg @author N.S. Oblath - @brief Contains information about sent messages + @brief Contains information about a sent message and, for requests, the mechanism to wait for a reply. @details - `core::send()` will return a `sent_msg_pkg`. + `core::send()` returns a `sent_msg_pkg_ptr` after sending a message. + + The result of the send is given by `f_successful_send` (true if the message was accepted for + delivery) and `f_send_error_message` (non-empty only on failure). - The result of act of sending the message is given by `f_successful_send` and `f_send_error_message`. + For **request** messages (`core::send( request_ptr_t )`): + - `f_reply_consumer` holds the rmqcpp consumer on a temporary auto-delete reply queue. + - `f_reply_promise` is fulfilled by the consumer's callback when the complete reply arrives. + - Call `receiver::wait_for_reply()` with this package to block until the reply is assembled. - Replies can be waited for and retried by passing the `sent_msg_pkg` to `receiver::wait_for_reply()`. + For **reply** and **alert** messages there is no reply expected: + - `f_reply_consumer` is null. + - `f_reply_promise` is null. + - `wait_for_reply()` returns immediately with a null `reply_ptr_t`. + + The destructor cancels `f_reply_consumer` if non-null, draining any in-flight reply messages. */ struct DRIPLINE_API sent_msg_pkg { - std::mutex f_mutex; - amqp_channel_ptr f_channel; - std::string f_consumer_tag; + bsl::shared_ptr< BloombergLP::rmqa::Consumer > f_reply_consumer; ///< rmqcpp consumer on the temporary reply queue (null if no reply expected) + std::shared_ptr< std::promise< reply_ptr_t > > f_reply_promise; ///< fulfilled by the reply consumer callback when the reply is assembled bool f_successful_send; std::string f_send_error_message; ~sent_msg_pkg(); @@ -50,57 +76,76 @@ namespace dripline @class core @author N.S. Oblath - @brief Basic AMQP interactions, including sending messages and interacting with AMQP channels. + @brief Basic AMQP interactions, including sending messages and setting up the AMQP topology. @details - The configuration for a `core` object is supplied via the constructor. The basic required information can be obtained - from `dripline_config`. The configuration values have default parameters, and they can be modified with the config - `param_node`, and a few parameters can be specified explicitly as constructor arguments. The order of precedence for - those values is (items higher in the list override those below them): + The configuration for a `core` object is supplied via the constructor. The basic required + information can be obtained from `dripline_config`. Configuration values have defaults that + can be overridden by the supplied `param_node`; a few can also be set explicitly as constructor + arguments. Precedence (highest first): * Constructor arguments (other than `a_config`) * Config `param_node` object * Defaults - - If the broker is not specified in either the config object or as a constructor parameter, it will be requested from the - authentication file. - - A second constructor allows a user to create a `core` object without connecting to a broker. - The primary user interface is `core::send()`, one of which exists for each type of message (request, reply, and alert). + If the broker address is not specified in either the config or as a constructor parameter, it + will be requested from the authentication file. + + **Topology management** + + `core` owns a single `rmqa::Topology` (`f_topology`, accessible read-only via `topology()`). + All AMQP queue and binding declarations for the service, monitor, or any other consumer must + be made through `core`'s helper functions so that they are recorded in `f_topology`. + `open_connection()` populates `f_topology` with both exchange declarations; subclasses then + add queues and bindings on top of that. The typical call sequence is: + + 1. `open_connection()` — establishes the broker connection and declares the two exchanges in + `f_topology`. + 2. `add_requests_[durable|ephemeral]_queue()` / `add_alerts_[durable|ephemeral]_queue()` — + adds a queue declaration to `f_topology` and returns a `QueueHandle`. + 3. `bind_requests_key()` / `bind_alerts_key()` — adds a binding to `f_topology`. + 4. Pass `f_topology` to `message_dispatcher::start_listening()` so that rmqcpp can redeclare + the full topology after a connection restart. - `Core` also contains a number of utility functions that wrap the main interactions with AMQP channels. - Classes wishing to take advantage of those functions should inherit from `core`. + **Durable vs. ephemeral queues** + + - *Durable* queues (`add_*_durable_queue()`) survive broker restarts. Use these when the + consumer needs to receive messages that arrived while it was offline. + - *Ephemeral* queues (`add_*_ephemeral_queue()`) are auto-delete and non-durable: they + disappear when the last consumer disconnects and are not restored after a broker restart. + `service` and `monitor` both use ephemeral queues because they process messages in real + time and have no need to buffer missed traffic. + + **Sending messages** + + The primary user interface is `core::send()`, one of which exists for each message type + (request, reply, and alert). Classes wishing to take advantage of the AMQP helpers should + inherit from `core`. */ class DRIPLINE_API core { public: static bool s_offline; - enum class post_listen_status - { - unknown, ///< Initialized or unknown status - message_received, ///< A message was received, and the channel is still valid - timeout, ///< A timeout occurred, and the channel is still valid - soft_error, ///< An error occurred, but the channel should still be valid - hard_error ///< An error occurred, and the channel is no longer valid - }; - public: - /* - \brief Extracts necessary configuration and authentication information and prepares the DL object to interact with the RabbitMQ broker. Does not initiate connection to the broker. - @param a_config Dripline configuration object. Contents can be: - - `broker` (string; default: localhost) -- Address of the RabbitMQ broker - - `broker_port` (int; default: 5672) -- Port used by the RabbitMQ broker - - `requests_exchange` (string; default: requests) -- Name of the exchange used for DL requests - - `alerts_exchange` (string; default: alerts) -- Name of the exchange used for DL alerts - - `heartbeat_routing_key` (string; default: heartbeat) -- Routing key used for sending heartbeats - - `make_connection` (bool; default: true) -- Flag for performing a dry run -- no connection to a broker is made; this parameter overrides the parameter in the constructor and is the preferred flag to use. - - `max_payload_size` (int; default: DL_MAX_PAYLOAD_SIZE) -- Maximum size of payloads, in bytes - - `max_connection_attempts` (int; default: 10) -- Maximum number of attempts that will be made to connect to the broker - - `return_codes` (string or array of nodes; default: not present) -- Optional specification of additional return codes in the form of an array of nodes: `[{name: "", value: } <, ...>]`. - If this is a string, it's treated as a file can be interpreted by the param system (e.g. YAML or JSON) using the previously-mentioned format - @param a_auth Authentication object (type scarab::authentication); authentication specification should be processed, and the authentication data should include: - @param a_make_connection Flag for whether or not to contact a broker; if true, this object operates in "dry-run" mode + /*! + @brief Extracts configuration and authentication information; prepares this object + to interact with the RabbitMQ broker. Does **not** open the connection. + + @param a_config Dripline configuration object. Recognised keys: + - `broker` (string; default: localhost) — broker address + - `broker_port` (int; default: 5672) — broker port + - `requests_exchange` (string; default: requests) — exchange for DL requests + - `alerts_exchange` (string; default: alerts) — exchange for DL alerts + - `heartbeat_routing_key` (string; default: heartbeat) — heartbeat routing key + - `make_connection` (bool; default: true) — if false, operates in dry-run mode + - `max_payload_size` (int; default: DL_MAX_PAYLOAD_SIZE) — max payload in bytes + - `max_connection_attempts` (int; default: 10) — connection attempt limit + - `return_codes` (string or array of nodes) — optional extra return codes as + `[{name: "", value: , description: ""}, ...]`. + A string value is treated as a file path to a YAML/JSON file. + @param a_auth Authentication object; should contain `dripline/username` and + `dripline/password` (defaults to `guest`/`guest`). + @param a_make_connection If false, operates in dry-run mode (overridden by config). */ core( const scarab::param_node& a_config = dripline_config(), const scarab::authentication& a_auth = scarab::authentication(), const bool a_make_connection = true ); core( const core& a_orig ) = default; @@ -111,29 +156,27 @@ namespace dripline core& operator=( core&& a_orig ) = default; public: - /// Sends a request message and returns a channel on which to listen for a reply. - /// Default exchange is "requests" - /// Caller can supply a channel; if one is not supplied, a new channel will be established - virtual sent_msg_pkg_ptr send( request_ptr_t a_request, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + /// Sends a request message; creates a temporary auto-delete reply queue and waits + /// for the reply via an rmqcpp consumer. Default exchange is "requests". + virtual sent_msg_pkg_ptr send( request_ptr_t a_request ) const; + + /// Sends a reply message. Default exchange is "requests". + virtual sent_msg_pkg_ptr send( reply_ptr_t a_reply ) const; + + /// Sends an alert message. Default exchange is "alerts". + virtual sent_msg_pkg_ptr send( alert_ptr_t a_alert ) const; - /// Sends a reply message - /// Default exchange is "requests" - /// Caller can supply a channel; if one is not supplied, a new channel will be established - virtual sent_msg_pkg_ptr send( reply_ptr_t a_reply, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + const std::string& requests_exchange() const; + std::string& requests_exchange(); - /// Sends an alert message - /// Default exchange is "alerts" - /// Caller can supply a channel; if one is not supplied, a new channel will be established - virtual sent_msg_pkg_ptr send( alert_ptr_t a_alert, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + const std::string& alerts_exchange() const; + std::string& alerts_exchange(); mv_referrable( std::string, address ); mv_accessible( unsigned, port ); mv_referrable( std::string, username ); mv_referrable( std::string, password ); - mv_referrable( std::string, requests_exchange ); - mv_referrable( std::string, alerts_exchange ); - mv_referrable( std::string, heartbeat_routing_key ); mv_accessible( unsigned, max_payload_size ); @@ -141,34 +184,183 @@ namespace dripline mv_accessible( bool, make_connection ); mv_accessible( unsigned, max_connection_attempts ); + public: + /*! + @brief Lazily establishes the RabbitMQ connection, declares both exchanges in + `f_topology`, and creates the requests and alerts producers. + @details Thread-safe (mutex-guarded); subsequent calls after the first successful + connection are no-ops. `f_topology` is populated here; all queue and + binding helpers must be called **after** this function. + */ + void open_connection() const; + + /*! + @brief Declares a **durable** queue on the requests exchange topology. + @details Must be called after `open_connection()`. Durable queues survive broker + restarts and buffer messages while the consumer is offline. Use for + consumers that must not miss traffic across restarts. + @param a_queue_name Unique name for this queue (e.g. a service or endpoint name). + @return QueueHandle to pass to `bind_requests_key()` and `message_dispatcher::start_listening()`. + */ + BloombergLP::rmqt::QueueHandle add_requests_queue( const std::string& a_queue_name, + bool a_auto_delete=false, bool a_durable=true, + const scarab::param_node& a_field_table=scarab::param_node(scarab::kwarg("x-single-active-consumer")=true) ); + + /*! + @brief Declares an **ephemeral** (auto-delete, non-durable) queue on the requests exchange topology. + @details Must be called after `open_connection()`. Ephemeral queues are removed when + the consumer disconnects and are not restored after a broker restart. + `service` uses this so that stale messages from a previous run are discarded. + @param a_queue_name Unique name for this queue (typically the service name). + @return QueueHandle to pass to `bind_requests_key()` and `message_dispatcher::start_listening()`. + */ + //BloombergLP::rmqt::QueueHandle add_requests_ephemeral_queue( const std::string& a_queue_name ); + + /*! + @brief Binds a queue on the requests exchange to the given routing key. + @details Must be called after `add_requests_durable_queue()` or + `add_requests_ephemeral_queue()` for the same queue. The routing key is + used verbatim — append `.#` yourself if you want wildcard matching. + @param a_queue_name Queue name (used in log messages). + @param a_routing_key Routing key pattern to bind. + @param a_queue QueueHandle returned by the corresponding `add_requests_*_queue()`. + */ + void bind_requests_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ); + + /*! + @brief Declares a **durable** queue on the alerts exchange topology. + @details Must be called after `open_connection()`. + @param a_queue_name Unique name for this queue. + @return QueueHandle to pass to `bind_alerts_key()` and `message_dispatcher::start_listening()`. + */ + BloombergLP::rmqt::QueueHandle add_alerts_queue( const std::string& a_queue_name, + bool a_auto_delete=false, bool a_durable=true, + const scarab::param_node& a_field_table=scarab::param_node(scarab::kwarg("x-single-active-consumer")=true) ); + + /*! + @brief Declares an **ephemeral** (auto-delete, non-durable) queue on the alerts exchange topology. + @details Must be called after `open_connection()`. + @param a_queue_name Unique name for this queue. + @return QueueHandle to pass to `bind_alerts_key()` and `message_dispatcher::start_listening()`. + */ + //BloombergLP::rmqt::QueueHandle add_alerts_ephemeral_queue( const std::string& a_queue_name ); + + /*! + @brief Binds a queue on the alerts exchange to the given routing key. + @details The queue handle may come from either `add_alerts_*_queue()` **or** from + `add_requests_*_queue()` — `monitor` intentionally binds a single queue to + both exchanges by calling both `bind_requests_key()` and `bind_alerts_key()` + with the same handle. + @param a_queue_name Queue name (used in log messages). + @param a_routing_key Routing key pattern to bind (verbatim). + @param a_queue QueueHandle to bind. + */ + void bind_alerts_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ); + protected: friend class receiver; - sent_msg_pkg_ptr do_send( message_ptr_t a_message, const std::string& a_exchange, bool a_expect_reply, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + /*! + @struct exchange_store + @brief Bundles the rmqcpp exchange handle and producer for a single AMQP exchange. - amqp_channel_ptr send_withreply( message_ptr_t a_message, std::string& a_reply_consumer_tag, const std::string& a_exchange ) const; - - bool send_noreply( message_ptr_t a_message, const std::string& a_exchange ) const; + @details + One `exchange_store` is held by `core` for the requests exchange (`f_requests_ex`) + and one for the alerts exchange (`f_alerts_ex`). Both are populated lazily inside + `open_connection()`. - amqp_channel_ptr open_channel() const; + Queue and binding declarations are made directly on `core::f_topology` (the single + shared topology), passed in by the `core` helpers. `exchange_store` no longer owns + a topology object. - static bool setup_exchange( amqp_channel_ptr a_channel, const std::string& a_exchange ); + The helper methods `add_durable_queue()`, `add_ephemeral_queue()`, and `bind_key()` + must only be called after `open_connection()` has populated `f_exchange`. + */ + struct exchange_store + { + BloombergLP::rmqa::Topology f_topology; + std::string f_name; ///< Exchange name (e.g. "requests" or "alerts") + BloombergLP::rmqt::ExchangeHandle f_exchange; + bsl::shared_ptr< BloombergLP::rmqa::Producer > f_producer; + + /*! + ... + */ + void create_exchange(); + + /*! + @brief Declares a durable (non-auto-delete) queue on the supplied topology. + @param a_topo The shared topology owned by `core`. + @param a_queue_name Unique name for the queue. + @return Handle to the newly declared queue. + */ + BloombergLP::rmqt::QueueHandle add_queue( const std::string& a_queue_name, + bool a_auto_delete, bool a_durable, + const scarab::param_node& a_field_table ); + + /*! + @brief Declares an ephemeral (auto-delete, non-durable) queue on the supplied topology. + @details Ephemeral queues are removed from the broker when the last consumer + disconnects and are not restored after a broker restart. They are + appropriate for real-time consumers such as `service` and `monitor`. + @param a_topo The shared topology owned by `core`. + @param a_queue_name Unique name for the queue (typically includes a UUID or service name). + @return Handle to the newly declared queue. + */ + //BloombergLP::rmqt::QueueHandle add_ephemeral_queue( BloombergLP::rmqa::Topology& a_topo, const std::string& a_queue_name ); + + /*! + @brief Binds a queue to this exchange with the supplied routing key. + @details The routing key is used verbatim; include any desired wildcard suffixes + (e.g. `"my-service.#"`). The queue handle may come from either + `add_durable_queue()` or `add_ephemeral_queue()` on **any** exchange + store — this is intentional, since `monitor` binds a single queue to + both the requests and the alerts exchange. + @param a_topo The shared topology owned by `core`. + @param a_queue_name Queue name (informational; used in error messages). + @param a_routing_key Routing key pattern to bind (verbatim). + @param a_queue Queue handle to bind. + */ + void bind_key( const std::string& a_queue_name, const std::string& a_routing_key, BloombergLP::rmqt::QueueHandle a_queue ); + }; - static bool setup_queue( amqp_channel_ptr a_channel, const std::string& a_queue_name ); + sent_msg_pkg_ptr do_send( message_ptr_t a_message, const std::string& a_exchange, bool a_expect_reply ) const; - static bool bind_key( amqp_channel_ptr a_channel, const std::string& a_exchange, const std::string& a_queue_name, const std::string& a_routing_key ); + /// Sets up a temporary reply queue, starts an rmqcpp consumer on it, then sends the + /// message. Stores the consumer and reply promise in `a_pkg`. + void send_withreply( message_ptr_t a_message, const std::string& a_exchange, sent_msg_pkg_ptr a_pkg ) const; - static std::string start_consuming( amqp_channel_ptr a_channel, const std::string& a_queue_name ); + bool send_noreply( message_ptr_t a_message, const std::string& a_exchange ) const; - static bool stop_consuming( amqp_channel_ptr a_channel, std::string& a_consumer_tag ); + mutable bsl::shared_ptr< BloombergLP::rmqa::RabbitContext > f_rabbit_context; + mutable bsl::shared_ptr< BloombergLP::rmqa::VHost > f_vhost; - static bool remove_queue( amqp_channel_ptr a_channel, const std::string& a_queue_name ); + mutable exchange_store f_requests_ex; + mutable exchange_store f_alerts_ex; - public: - /// listen for a single AMQP message - static void listen_for_message( amqp_envelope_ptr& a_envelope, post_listen_status& a_status, amqp_channel_ptr a_channel, const std::string& a_consumer_tag, int a_timeout_ms = 0, bool a_do_ack = true ); + mutable std::shared_ptr< std::mutex > f_connection_mutex; }; + inline const std::string& core::requests_exchange() const + { + return f_requests_ex.f_name; + } + + inline std::string& core::requests_exchange() + { + return f_requests_ex.f_name; + } + + inline const std::string& core::alerts_exchange() const + { + return f_alerts_ex.f_name; + } + + inline std::string& core::alerts_exchange() + { + return f_alerts_ex.f_name; + } + } /* namespace dripline */ #endif /* DRIPLINE_CORE_HH_ */ diff --git a/library/dripline_fwd.hh b/library/dripline_fwd.hh index 5f76fd4e..839494d2 100644 --- a/library/dripline_fwd.hh +++ b/library/dripline_fwd.hh @@ -27,12 +27,6 @@ namespace dripline struct sent_msg_pkg; typedef std::shared_ptr< sent_msg_pkg > sent_msg_pkg_ptr; - class listener; - typedef std::shared_ptr< listener > listener_ptr_t; - - class listener_receiver; - typedef std::shared_ptr< listener_receiver > lr_ptr_t; - class endpoint_listener_receiver; typedef std::shared_ptr< endpoint_listener_receiver > elr_ptr_t; diff --git a/library/endpoint.cc b/library/endpoint.cc index 85e617b7..5e6c705e 100644 --- a/library/endpoint.cc +++ b/library/endpoint.cc @@ -431,4 +431,43 @@ namespace dripline return a_request->reply( dl_success(), "Hello, " + a_request->sender_exe() ); } + endpoint_listener_receiver::endpoint_listener_receiver( endpoint_ptr_t a_endpoint_ptr ) : + message_dispatcher(), + f_endpoint( a_endpoint_ptr ) + {} + + endpoint_listener_receiver::endpoint_listener_receiver( endpoint_listener_receiver&& a_orig ) : + message_dispatcher( std::move(a_orig) ), + f_endpoint( std::move(a_orig.f_endpoint) ) + {} + + endpoint_listener_receiver::~endpoint_listener_receiver() + {} + + endpoint_listener_receiver& endpoint_listener_receiver::operator=( endpoint_listener_receiver&& a_orig ) + { + message_dispatcher::operator=( std::move(a_orig) ); + f_endpoint = std::move(a_orig.f_endpoint); + return *this; + } + + void endpoint_listener_receiver::submit_message( message_ptr_t a_message ) + { + try + { + f_endpoint->sort_message( a_message ); + return; + } + catch( dripline_error& e ) + { + LERROR( dlog, "<" << f_endpoint->name() << ">: Dripline exception caught while handling message: " << e.what() ); + throw; + } + catch( std::exception& e ) + { + LERROR( dlog, "<" << f_endpoint->name() << ">: Standard exception caught while handling message: " << e.what() ); + throw; + } + } + } /* namespace dripline */ diff --git a/library/endpoint.hh b/library/endpoint.hh index e5ce09d2..df28358a 100644 --- a/library/endpoint.hh +++ b/library/endpoint.hh @@ -9,6 +9,7 @@ #define DRIPLINE_ENDPOINT_HH_ #include "message.hh" +#include "message_dispatcher.hh" #include "return_codes.hh" namespace dripline @@ -252,6 +253,52 @@ namespace dripline return a_request->reply( dl_success(), "No action taken (this is the default method)" ); } + /*! + @class endpoint_listener_receiver + @author N.S. Oblath + + @brief Decorator class for a plain endpoint: adds message_dispatcher capabilities for asynchronous message consumption. + + @details + `endpoint_listener_receiver` (ELR) is the bridge between the AMQP consumer layer + (`message_dispatcher`) and a user-supplied `endpoint`. It is used by @ref service to + wrap any endpoint that is registered as an **asynchronous child** via + `service::add_async_child()`. + + An endpoint passed to `add_async_child()` is either already an ELR (in which case it + is used directly) or is wrapped in a new ELR automatically. The resulting ELR: + + - Gets its own AMQP queue declared by `service::add_queues()`. + - Gets its own set of routing-key bindings added by `service::bind_keys()`. + - Gets its own rmqcpp consumer started by `service::listen()` via + `message_dispatcher::start_listening()`. + - Receives messages independently of the service's main queue; its + `submit_message()` implementation forwards each assembled Dripline message to the + decorated endpoint's `sort_message()`. + + The `f_queue` member (inherited from `message_dispatcher`) is populated by + `service::add_queues()` before `start_listening()` is called. The topology + (`core::f_topology`, owned by the enclosing `service`) is passed directly to + `start_listening()` — the ELR does not own a separate topology object. + */ + class DRIPLINE_API endpoint_listener_receiver : public message_dispatcher + { + public: + explicit endpoint_listener_receiver( endpoint_ptr_t a_endpoint_ptr ); + endpoint_listener_receiver( const endpoint_listener_receiver& ) = delete; + endpoint_listener_receiver( endpoint_listener_receiver&& a_orig ); + virtual ~endpoint_listener_receiver(); + + endpoint_listener_receiver& operator=( const endpoint_listener_receiver& ) = delete; + endpoint_listener_receiver& operator=( endpoint_listener_receiver&& a_orig ); + + /// Direct submission of messages to the endpoint + virtual void submit_message( message_ptr_t a_message ) override; + + /// Pointer to the decorated endpoint + mv_referrable( endpoint_ptr_t, endpoint ); + }; + } /* namespace dripline */ #endif /* DRIPLINE_ENDPOINT_HH_ */ diff --git a/library/listener.cc b/library/listener.cc deleted file mode 100644 index 233bc902..00000000 --- a/library/listener.cc +++ /dev/null @@ -1,148 +0,0 @@ -/* - * listener.cc - * - * Created on: Jun 23, 2019 - * Author: N.S. Oblath - */ - -#define DRIPLINE_API_EXPORTS - -#include "listener.hh" - -#include "endpoint.hh" - -#include "core.hh" -#include "dripline_exceptions.hh" - -#include "logger.hh" - -namespace dripline -{ - LOGGER( dlog, "listener" ); - - listener::listener() : - cancelable(), - f_channel(), - f_consumer_tag(), - f_listen_timeout_ms( 1000 ), - f_listener_thread() - {} - - listener& listener::operator=( listener&& a_orig ) - { - cancelable::operator=( std::move(a_orig) ); - f_channel = std::move( a_orig.f_channel ); - f_consumer_tag = std::move( a_orig.f_consumer_tag ); - f_listen_timeout_ms = a_orig.f_listen_timeout_ms; - f_listener_thread = std::move( a_orig.f_listener_thread ); - return *this; - } - - endpoint_listener_receiver::endpoint_listener_receiver( endpoint_ptr_t a_endpoint_ptr ) : - scarab::cancelable(), - listener_receiver(), - f_endpoint( a_endpoint_ptr ) - {} - - endpoint_listener_receiver::endpoint_listener_receiver( endpoint_listener_receiver&& a_orig ) : - scarab::cancelable( std::move(a_orig) ), - listener_receiver( std::move(a_orig) ), - f_endpoint( std::move(a_orig.f_endpoint) ) - {} - - endpoint_listener_receiver::~endpoint_listener_receiver() - {} - - endpoint_listener_receiver& endpoint_listener_receiver::operator=( endpoint_listener_receiver&& a_orig ) - { - listener_receiver::operator=( std::move(a_orig) ); - f_endpoint = std::move(a_orig.f_endpoint); - return *this; - } - - bool endpoint_listener_receiver::listen_on_queue() - { - LINFO( dlog, "Listening for incoming messages on <" << f_endpoint->name() << ">" ); - - while( ! f_canceled.load() ) - { - - amqp_envelope_ptr t_envelope; - core::post_listen_status t_post_listen_status = core::post_listen_status::unknown; - core::listen_for_message( t_envelope, t_post_listen_status, f_channel, f_consumer_tag, f_listen_timeout_ms ); - - if( f_canceled.load() ) - { - LDEBUG( dlog, "Service canceled" ); - return true; - } - - if( t_post_listen_status == core::post_listen_status::timeout ) - { - // we end up here every time the listen times out with no message received - continue; - } - - if( t_post_listen_status == core::post_listen_status::soft_error ) - { - LWARN( dlog, "A soft error ocurred while listening for messages for <" << f_endpoint->name() << ">. The channel is still valid" ); - continue; - } - - if( t_post_listen_status == core::post_listen_status::hard_error ) - { - LERROR( dlog, "A hard error ocurred while listening for messages for <" << f_endpoint->name() << ">. The channel is no longer valid" ); - return false; - } - - if( t_post_listen_status == core::post_listen_status::unknown ) - { - LERROR( dlog, "An unknown status occurred while listening for messages for <" << f_endpoint->name() << ">" ); - return false; - } - - // remaining status is core::post_listen_status::message_received - - handle_message_chunk( t_envelope ); - - if( f_canceled.load() ) - { - LDEBUG( dlog, "Listener <" << f_endpoint->name() << "> canceled" ); - return true; - } - } - return true; - } - - void endpoint_listener_receiver::submit_message( message_ptr_t a_message ) - { - try - { - f_endpoint->sort_message( a_message ); - return; - } - catch( dripline_error& e ) - { - LERROR( dlog, "<" << f_endpoint->name() << ">: Dripline exception caught while handling message: " << e.what() ); - throw; - } - catch( amqp_exception& e ) - { - LERROR( dlog, "<" << f_endpoint->name() << ">: AMQP exception caught while handling message: (" << e.reply_code() << ") " << e.reply_text() ); - throw; - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "<" << f_endpoint->name() << ">: AMQP Library Exception caught while handling message: (" << e.ErrorCode() << ") " << e.what() ); - throw; - } - catch( std::exception& e ) - { - LERROR( dlog, "<" << f_endpoint->name() << ">: Standard exception caught while handling message: " << e.what() ); - throw; - } - - return; - } - -} /* namespace dripline */ diff --git a/library/listener.hh b/library/listener.hh deleted file mode 100644 index 82e1305b..00000000 --- a/library/listener.hh +++ /dev/null @@ -1,132 +0,0 @@ -/* - * listener.hh - * - * Created on: Jun 23, 2019 - * Author: N.S. Oblath - */ - -#ifndef DRIPLINE_LISTENER_HH_ -#define DRIPLINE_LISTENER_HH_ - -#include "dripline_fwd.hh" - -#include "receiver.hh" - -#include "cancelable.hh" -#include "member_variables.hh" - -#include - -namespace dripline -{ - /*! - @class listener - @author N.S. Oblath - - @brief A listener is a class capable of listening for AMQP messages on an AMQP channel. This class provides the - basic framework for doing that. - - @details - The listener class is a mix-in class that provides the interface for listening for messages: - the pure virtual function `listen_on_queue()`. - This function should be run in the thread provided by this class. - However, it needs to be implemented by the inheriting class. - - The typical use case involves at least two threads: - 1. A listener gets messages from the AMQP channel (using `listen_on_queue(), e.g. @ref service or @ref endpoint_listener_receiver) and - calls `receiver::handle_message_chunk()` - 2. A receiver has a timing thread waiting for multiple message chunks (if relevant); - when the message is complete, `receiver::process_message()` is called. - - This class also provides the objects and information needed for listening on a queue: - * The AMQP channel - * A consumer tag - * A timeout (in ms) - * The thread object for listening - */ - class DRIPLINE_API listener : public virtual scarab::cancelable - { - public: - listener(); - listener( const listener& ) = delete; - listener( listener&& a_orig ) = default; - virtual ~listener() = default; - - listener& operator=( const listener& ) = delete; - listener& operator=( listener&& a_orig ); - - /// Returns false if the return is due to an error in this function; returns true otherwise (namely because it was canceled) - virtual bool listen_on_queue() = 0; - - mv_referrable( amqp_channel_ptr, channel ); - - mv_referrable( std::string, consumer_tag ); - - mv_accessible( unsigned, listen_timeout_ms ); - - mv_referrable( std::thread, listener_thread ); - }; - - /*! - @class listener_receiver - @author N.S. Oblath - - @brief Convenience class to bring together @ref listener and @ref concurrent_receiver - */ - class DRIPLINE_API listener_receiver : public listener, public concurrent_receiver - { - public: - listener_receiver() : scarab::cancelable(), listener(), concurrent_receiver() {} - listener_receiver( const listener_receiver& ) = delete; - listener_receiver( listener_receiver&& a_orig ) : - scarab::cancelable( std::move(a_orig) ), - listener( std::move(a_orig) ), - concurrent_receiver( std::move(a_orig) ) - {} - - listener_receiver& operator=( const listener_receiver& ) = delete; - listener_receiver& operator=( listener_receiver&& a_orig ) - { - listener::operator=( std::move(a_orig) ); - concurrent_receiver::operator=( std::move(a_orig) ); - return *this; - } - }; - - /*! - @class endpoint_listener_receiver - @author N.S. Oblath - - @brief Decorator class for a plain endpoint: adds listener_receiver capabilities. - - @details - The endpoint_listener_receiver is used by @ref service to wrap an endpoint that is to listen for messages asynchronously. - */ - class DRIPLINE_API endpoint_listener_receiver : public listener_receiver - { - public: - endpoint_listener_receiver( endpoint_ptr_t a_endpoint_ptr ); - endpoint_listener_receiver( const endpoint_listener_receiver& ) = delete; - endpoint_listener_receiver( endpoint_listener_receiver&& a_orig ); - virtual ~endpoint_listener_receiver(); - - endpoint_listener_receiver& operator=( const endpoint_listener_receiver& ) = delete; - endpoint_listener_receiver& operator=( endpoint_listener_receiver&& a_orig ); - - /// Listens for AMQP messages and then passes them to be handled as Dripline message chunks - /// Returns false if the return is due to an error in this function; returns true otherwise (namely because it was canceled) - virtual bool listen_on_queue(); - - protected: - /// Direct submission of messages to the endpoint - virtual void submit_message( message_ptr_t a_message ); - - /// Pointer to the decorated endpoint - mv_referrable( endpoint_ptr_t, endpoint ); - }; - - - -} /* namespace dripline */ - -#endif /* DRIPLINE_LISTENER_HH_ */ diff --git a/library/message.cc b/library/message.cc index 67d4f48b..f7d44598 100644 --- a/library/message.cc +++ b/library/message.cc @@ -21,6 +21,9 @@ #include "time.hh" #include "version_wrapper.hh" +#include "bsl_vector.h" +#include "rmqt_properties.h" + #include #include @@ -101,16 +104,6 @@ namespace dripline } } -/* - message_ptr_t message::process_envelope( amqp_envelope_ptr a_envelope ) - { - if( ! a_envelope ) - { - throw dripline_error() << "Empty envelope received"; - } - return message::process_message( a_envelope->Message(), a_envelope->RoutingKey() ); - } -*/ std::tuple< std::string, unsigned, unsigned > message::parse_message_id( const string& a_message_id ) { std::string::size_type t_first_separator = a_message_id.find_first_of( s_message_id_separator ); @@ -152,16 +145,18 @@ namespace dripline throw dripline_error() << "All messages provided for processing were invalid"; } - unsigned t_payload_chunk_length = t_first_valid_message->Body().size(); + unsigned t_payload_chunk_length = t_first_valid_message->payloadSize(); encoding t_encoding; - if( t_first_valid_message->ContentEncoding() == "application/json" ) + const std::string t_content_encoding = t_first_valid_message->properties().contentEncoding.isNull() ? "" : + std::string( t_first_valid_message->properties().contentEncoding.value() ); + if( t_content_encoding == "application/json" ) { t_encoding = encoding::json; } else { - throw dripline_error() << "Unable to parse message with content type <" << t_first_valid_message->ContentEncoding() << ">"; + throw dripline_error() << "Unable to parse message with content type <" << t_content_encoding << ">"; } // Build up the body @@ -177,7 +172,7 @@ namespace dripline continue; } - t_payload_str += t_message->Body(); + t_payload_str += std::string( reinterpret_cast< const char* >( t_message->payload() ), t_message->payloadSize() ); } // Attempt to parse @@ -214,28 +209,46 @@ namespace dripline using scarab::at; - using AmqpClient::Table; - using AmqpClient::TableEntry; - using AmqpClient::TableValue; - Table t_properties = t_first_valid_message->HeaderTable(); + const auto& t_hdr_ptr = t_first_valid_message->headers(); + const BloombergLP::rmqt::FieldTable& t_properties = t_hdr_ptr ? *t_hdr_ptr : BloombergLP::rmqt::FieldTable(); + + auto t_hdr_uint = [&t_properties]( const char* a_key, unsigned a_default ) -> unsigned { + auto it = t_properties.find( bsl::string(a_key) ); + if( it == t_properties.end() || !it->second.is< bsl::uint32_t >() ) return a_default; + return (unsigned)it->second.the< bsl::uint32_t >(); + }; + auto t_hdr_str = [&t_properties]( const char* a_key, const char* a_default ) -> std::string { + auto it = t_properties.find( bsl::string(a_key) ); + if( it == t_properties.end() || !it->second.is< bsl::string >() ) return a_default; + return std::string( it->second.the< bsl::string >() ); + }; + auto t_hdr_table = [&t_properties]( const char* a_key ) -> const BloombergLP::rmqt::FieldTable* { + auto it = t_properties.find( bsl::string(a_key) ); + if( it == t_properties.end() || + !it->second.is< bsl::shared_ptr< BloombergLP::rmqt::FieldTable > >() ) return nullptr; + const auto& p = it->second.the< bsl::shared_ptr< BloombergLP::rmqt::FieldTable > >(); + return p ? p.get() : nullptr; + }; // Create the message, of whichever type message_ptr_t t_message; - msg_t t_msg_type = to_msg_t( at( t_properties, std::string("message_type"), TableValue(to_uint(msg_t::unknown)) ).GetInteger() ); + msg_t t_msg_type = to_msg_t( t_hdr_uint("message_type", to_uint(msg_t::unknown)) ); switch( t_msg_type ) { case msg_t::request: { + const std::string t_reply_to = t_first_valid_message->properties().replyTo.isNull() ? "" : + std::string( t_first_valid_message->properties().replyTo.value() ); request_ptr_t t_request = msg_request::create( std::move(t_payload), - to_op_t( at( t_properties, std::string("message_operation"), TableValue(to_uint(op_t::unknown)) ).GetInteger() ), + to_op_t( t_hdr_uint("message_operation", to_uint(op_t::unknown)) ), a_routing_key, - at( t_properties, std::string("specifier"), TableValue("") ).GetString(), - t_first_valid_message->ReplyTo(), + t_hdr_str("specifier", ""), + t_reply_to, t_encoding); bool t_lockout_key_valid = true; - t_request->lockout_key() = uuid_from_string( at( t_properties, std::string("lockout_key"), TableValue("") ).GetString(), t_lockout_key_valid ); + t_request->lockout_key() = uuid_from_string( t_hdr_str("lockout_key", ""), t_lockout_key_valid ); t_request->set_lockout_key_valid( t_lockout_key_valid ); t_message = t_request; @@ -244,11 +257,11 @@ namespace dripline case msg_t::reply: { reply_ptr_t t_reply = msg_reply::create( - at( t_properties, std::string("return_code"), TableValue(999U) ).GetInteger(), - at( t_properties, std::string("return_message"), TableValue("") ).GetString(), + t_hdr_uint("return_code", 999U), + t_hdr_str("return_message", ""), std::move(t_payload), a_routing_key, - at( t_properties, std::string("specifier"), TableValue("") ).GetString(), + t_hdr_str("specifier", ""), t_encoding); t_message = t_reply; @@ -259,7 +272,7 @@ namespace dripline alert_ptr_t t_alert = msg_alert::create( std::move(t_payload), a_routing_key, - at( t_properties, std::string("specifier"), TableValue("") ).GetString(), + t_hdr_str("specifier", ""), t_encoding); t_message = t_alert; @@ -278,17 +291,19 @@ namespace dripline t_message->set_is_valid( false ); } - t_message->correlation_id() = t_first_valid_message->CorrelationId(); - t_message->message_id() = t_first_valid_message->MessageId(); + t_message->correlation_id() = t_first_valid_message->properties().correlationId.isNull() ? "" : + std::string( t_first_valid_message->properties().correlationId.value() ); + t_message->message_id() = std::string( t_first_valid_message->messageId() ); // remove the message chunk information from the message id t_message->message_id() = t_message->message_id().substr( 0, t_message->message_id().find_first_of(s_message_id_separator) ); - t_message->timestamp() = at( t_properties, std::string("timestamp"), TableValue("") ).GetString(); + t_message->timestamp() = t_hdr_str("timestamp", ""); - Table t_sender_info = at( t_properties, std::string("sender_info"), TableValue(Table()) ).GetTable(); - scarab::param_ptr_t t_sender_info_param = table_to_param( t_sender_info ); - t_message->set_sender_info( t_sender_info_param->as_node() ); - - t_message->payload() = *t_payload; + const BloombergLP::rmqt::FieldTable* t_si_tbl = t_hdr_table("sender_info"); + if( t_si_tbl ) + { + scarab::param_ptr_t t_sender_info_param = table_to_param( *t_si_tbl ); + t_message->set_sender_info( t_sender_info_param->as_node() ); + } return t_message; } @@ -315,22 +330,32 @@ namespace dripline unsigned i_chunk = 0; for( string& t_body_part : t_body_parts ) { - amqp_message_ptr t_message = AmqpClient::BasicMessage::Create( t_body_part ); + // Build raw payload bytes + auto t_raw_data = bsl::make_shared< bsl::vector< bsl::uint8_t > >( + t_body_part.begin(), t_body_part.end() ); + + // Build AMQP properties + BloombergLP::rmqt::Properties t_props; + t_props.contentEncoding = bsl::string( interpret_encoding() ); + t_props.correlationId = bsl::string( f_correlation_id ); + t_props.messageId = bsl::string( t_base_message_id + std::to_string(i_chunk) + t_total_chunks_str ); + if( ! f_reply_to.empty() ) + { + t_props.replyTo = bsl::string( f_reply_to ); + } - t_message->ContentEncoding( interpret_encoding() ); - t_message->CorrelationId( f_correlation_id ); - t_message->MessageId( t_base_message_id + std::to_string(i_chunk) + t_total_chunks_str ); - t_message->ReplyTo( f_reply_to ); + // Build header FieldTable + auto t_headers = bsl::make_shared< BloombergLP::rmqt::FieldTable >(); + (*t_headers)[bsl::string("message_type")] = BloombergLP::rmqt::FieldValue( (bsl::uint32_t)to_uint(message_type()) ); + (*t_headers)[bsl::string("specifier")] = BloombergLP::rmqt::FieldValue( bsl::string(f_specifier.to_string()) ); + (*t_headers)[bsl::string("timestamp")] = BloombergLP::rmqt::FieldValue( bsl::string(f_timestamp) ); + (*t_headers)[bsl::string("sender_info")] = param_to_table( get_sender_info() ); - AmqpClient::Table t_properties; - t_properties.insert( AmqpClient::TableEntry( "message_type", to_uint(message_type()) ) ); - t_properties.insert( AmqpClient::TableEntry( "specifier", f_specifier.to_string() ) ); - t_properties.insert( AmqpClient::TableEntry( "timestamp", f_timestamp ) ); - t_properties.insert( AmqpClient::TableEntry( "sender_info", param_to_table( get_sender_info() ) ) ); + this->derived_modify_amqp_message( *t_headers ); - this->derived_modify_amqp_message( t_message, t_properties ); + t_props.headers = t_headers; - t_message->HeaderTable( t_properties ); + amqp_message_ptr t_message = bsl::make_shared< BloombergLP::rmqt::Message >( t_raw_data, t_props ); t_message_parts[i_chunk] = t_message; diff --git a/library/message.hh b/library/message.hh index c6fe091b..b8c46a66 100644 --- a/library/message.hh +++ b/library/message.hh @@ -88,7 +88,7 @@ namespace dripline std::string encode_full_message( unsigned a_max_size, const scarab::param_node& a_options = scarab::param_node() ) const; protected: - virtual void derived_modify_amqp_message( amqp_message_ptr a_amqp_msg, AmqpClient::Table& a_properties ) const = 0; + virtual void derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const = 0; virtual void derived_modify_message_param( scarab::param_node& a_message_node ) const = 0; @@ -200,7 +200,7 @@ namespace dripline reply_ptr_t reply( const unsigned a_return_code, const std::string& a_ret_msg, scarab::param_ptr_t a_payload = scarab::param_ptr_t( new scarab::param() ) ) const; private: - void derived_modify_amqp_message( amqp_message_ptr a_amqp_msg, AmqpClient::Table& a_properties ) const; + void derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const; virtual void derived_modify_message_param( scarab::param_node& a_message_node ) const; public: @@ -261,7 +261,7 @@ namespace dripline bool is_alert() const; private: - void derived_modify_amqp_message( amqp_message_ptr a_amqp_msg, AmqpClient::Table& a_properties ) const; + void derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const; virtual void derived_modify_message_param( scarab::param_node& a_message_node ) const; public: @@ -315,7 +315,7 @@ namespace dripline bool is_alert() const; private: - void derived_modify_amqp_message( amqp_message_ptr a_amqp_msg, AmqpClient::Table& a_properties ) const; + void derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const; virtual void derived_modify_message_param( scarab::param_node& a_message_node ) const; public: @@ -381,10 +381,10 @@ namespace dripline return false; } - inline void msg_request::derived_modify_amqp_message( amqp_message_ptr /*a_amqp_msg*/, AmqpClient::Table& a_properties ) const + inline void msg_request::derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const { - a_properties.insert( AmqpClient::TableEntry( "message_operation", AmqpClient::TableValue(to_uint(f_message_operation)) ) ); - a_properties.insert( AmqpClient::TableEntry( "lockout_key", AmqpClient::TableValue(string_from_uuid(lockout_key())) ) ); + a_headers[bsl::string("message_operation")] = BloombergLP::rmqt::FieldValue( (bsl::uint32_t)to_uint(f_message_operation) ); + a_headers[bsl::string("lockout_key")] = BloombergLP::rmqt::FieldValue( bsl::string(string_from_uuid(lockout_key())) ); return; } @@ -435,10 +435,10 @@ namespace dripline return false; } - inline void msg_reply::derived_modify_amqp_message( amqp_message_ptr, AmqpClient::Table& a_properties ) const + inline void msg_reply::derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& a_headers ) const { - a_properties.insert( AmqpClient::TableEntry( "return_code", AmqpClient::TableValue(f_return_code) ) ); - a_properties.insert( AmqpClient::TableEntry( "return_message", AmqpClient::TableValue(f_return_message) ) ); + a_headers[bsl::string("return_code")] = BloombergLP::rmqt::FieldValue( (bsl::uint32_t)f_return_code ); + a_headers[bsl::string("return_message")] = BloombergLP::rmqt::FieldValue( bsl::string(f_return_message) ); return; } @@ -467,7 +467,7 @@ namespace dripline return true; } - inline void msg_alert::derived_modify_amqp_message( amqp_message_ptr, AmqpClient::Table& ) const + inline void msg_alert::derived_modify_amqp_message( BloombergLP::rmqt::FieldTable& ) const { return; } diff --git a/library/message_dispatcher.cc b/library/message_dispatcher.cc new file mode 100644 index 00000000..6a3cd7fa --- /dev/null +++ b/library/message_dispatcher.cc @@ -0,0 +1,89 @@ +/* + * message_dispatcher.cc + * + * Created on: Jun 3, 2026 + * Author: N.S. Oblath + */ + +#define DRIPLINE_API_EXPORTS + +#include "message_dispatcher.hh" + +#include "dripline_exceptions.hh" + +#include "rmqa_consumer.h" +#include "rmqa_topology.h" +#include "rmqa_vhost.h" +#include "rmqp_messageguard.h" + +#include "logger.hh" + +LOGGER( dlog, "message_dispatcher" ); + +namespace dripline +{ + + message_dispatcher::message_dispatcher() : + receiver(), + f_queue(), + f_consumer() + {} + + message_dispatcher::message_dispatcher( message_dispatcher&& a_orig ) : + receiver( std::move(a_orig) ), + f_queue( std::move(a_orig.f_queue) ), + f_consumer( std::move(a_orig.f_consumer) ) + {} + + message_dispatcher::~message_dispatcher() + {} + + message_dispatcher& message_dispatcher::operator=( message_dispatcher&& a_orig ) + { + receiver::operator=( std::move(a_orig) ); + f_queue = std::move(a_orig.f_queue); + f_consumer = std::move(a_orig.f_consumer); + return *this; + } + + void message_dispatcher::process_message( message_ptr_t a_message ) + { + this->submit_message( a_message ); + return; + } + + void message_dispatcher::start_listening( bsl::shared_ptr< BloombergLP::rmqa::VHost > a_vhost, + BloombergLP::rmqa::Topology& a_topology, + const std::string& a_label ) + { + using namespace BloombergLP; + auto t_result = a_vhost->createConsumer( + a_topology, f_queue, + [this]( rmqp::MessageGuard& guard ) { + amqp_envelope_ptr t_envelope = guard.transferOwnership(); + t_envelope->ack(); + handle_message_chunk( std::move(t_envelope) ); + }, + a_label, + 1 ); + if( ! t_result ) + { + throw connection_error() << "Unable to create consumer: " << t_result.error(); + } + f_consumer = t_result.value(); + } + + void message_dispatcher::stop_listening() + { + if( f_consumer ) + { + auto t_result = f_consumer->cancelAndDrain(); + if( ! t_result ) + { + LWARN( dlog, "Error canceling consumer: " << t_result.error() ); + } + f_consumer.reset(); + } + } + +} /* namespace dripline */ diff --git a/library/message_dispatcher.hh b/library/message_dispatcher.hh new file mode 100644 index 00000000..1828da22 --- /dev/null +++ b/library/message_dispatcher.hh @@ -0,0 +1,125 @@ +/* + * message_dispatcher.hh + * + * Created on: Jun 3, 2026 + * Author: N.S. Oblath + */ + +#ifndef DRIPLINE_MESSAGE_DISPATCHER_HH_ +#define DRIPLINE_MESSAGE_DISPATCHER_HH_ + +#include "receiver.hh" + +namespace BloombergLP { + namespace rmqa { class Consumer; class RabbitContext; class VHost; class Producer; } + namespace rmqt + { + class Queue; + typedef bsl::weak_ptr QueueHandle; + } +} + +namespace dripline +{ + + /*! + @class message_dispatcher + @author N.S. Oblath + + @brief Receives assembled Dripline messages from an AMQP consumer and dispatches them. + + @details + This class manages the lifecycle of an rmqcpp Consumer (via `start_listening()` / + `stop_listening()`) and overrides `process_message()` to dispatch each assembled + Dripline message directly to `submit_message()` (synchronously in the rmqcpp + callback thread). + + A class deriving from `message_dispatcher` must implement `submit_message()` to + define what happens with each received message. + + **Topology ownership** + + `message_dispatcher` does **not** own the AMQP topology. The topology is owned by + the `core` instance that the concrete subclass (e.g. `service`, `monitor`) also + inherits from. The queue handle `f_queue` is set by the subclass via + `core::add_requests_ephemeral_queue()` (or the durable variant) and then the shared + `core::f_topology` is passed to `start_listening()`. This means that after a + connection restart rmqcpp will redeclare the complete topology — exchanges, queues, + and bindings — from the single canonical object in `core`. + + The typical call sequence (performed by `service`) is: + 1. `open_connection()` — establishes the connection and declares exchanges. + 2. `add_queues()` — calls `core::add_requests_ephemeral_queue()` → stores result in `f_queue`. + 3. `bind_keys()` — calls `core::bind_requests_key()` with `f_queue`. + 4. `start_listening( f_vhost, core::f_topology, label )` — starts consuming. + + @note + The name `concurrent_receiver` was used for this class prior to the rmqcpp migration. + After the migration the class no longer manages any concurrency itself — rmqcpp's + internal thread pool handles delivery — so it was renamed to `message_dispatcher`. + */ + class DRIPLINE_API message_dispatcher : public receiver + { + public: + message_dispatcher(); + message_dispatcher( const message_dispatcher& ) = delete; + message_dispatcher( message_dispatcher&& a_orig ); + virtual ~message_dispatcher(); + + message_dispatcher& operator=( const message_dispatcher& ) = delete; + message_dispatcher& operator=( message_dispatcher&& a_orig ); + + public: + /// Dispatches the message directly to `submit_message()`. + virtual void process_message( message_ptr_t a_message ); + + /*! + @brief Creates an rmqcpp consumer using the supplied topology and this dispatcher's + queue handle, then begins receiving messages. + + @details + `f_queue` must be set (via `core::add_requests_ephemeral_queue()` or similar) and + the bindings must be added to `a_topology` (via `core::bind_requests_key()` etc.) + before calling this method. + + The topology is passed by (non-const) reference because rmqcpp requires it that + way; the underlying `Topology` object is owned by `core::f_topology`. + + Throws `connection_error` if the rmqcpp consumer cannot be created. + + @param a_vhost The rmqcpp VHost to create the consumer on. + @param a_topology The shared topology (from `core::f_topology`) that rmqcpp uses + to redeclare exchanges, queues, and bindings after reconnects. + @param a_label Optional label for the consumer (used for logging/identification). + */ + void start_listening( bsl::shared_ptr< BloombergLP::rmqa::VHost > a_vhost, + BloombergLP::rmqa::Topology& a_topology, + const std::string& a_label = "" ); + + /*! + @brief Cancels the rmqcpp consumer, drains in-flight messages, and releases it. + + @details + This method is idempotent: it is safe to call even if `start_listening()` was never + called or if the consumer has already been stopped. + */ + void stop_listening(); + + /// Handles messages according to the use case. Must be implemented by the + /// class inheriting from message_dispatcher. + /// For a concrete example, see @ref service or @ref endpoint_listener_receiver. + virtual void submit_message( message_ptr_t a_message ) = 0; + + // The queue this dispatcher consumes from. + // Set by the concrete subclass (e.g. via `service::add_queues()` calling + // `core::add_requests_ephemeral_queue()`) before `start_listening()` is called. + mv_accessible( BloombergLP::rmqt::QueueHandle, queue ); + + protected: + bsl::shared_ptr< BloombergLP::rmqa::Consumer > f_consumer; + + }; + +} /* namespace dripline */ + +#endif /* DRIPLINE_MESSAGE_DISPATCHER_HH_ */ diff --git a/library/monitor.cc b/library/monitor.cc index 5ac79902..542da2a1 100644 --- a/library/monitor.cc +++ b/library/monitor.cc @@ -15,6 +15,9 @@ #include "logger.hh" #include "signal_handler.hh" +#include +#include + LOGGER( dlog, "monitor" ); namespace dripline @@ -23,7 +26,7 @@ namespace dripline monitor::monitor( const scarab::param_node& a_config, const scarab::authentication& a_auth ) : scarab::cancelable(), core( a_config["dripline_mesh"].as_node(), a_auth ), - listener_receiver(), + message_dispatcher(), f_status( status::nothing ), f_name( std::string("monitor_") + string_from_uuid(generate_random_uuid()) ), f_json_print( false ), @@ -53,7 +56,7 @@ namespace dripline if( a_config.has( "alert_keys" ) && a_config["alert_keys"].is_array() ) { const scarab::param_array& t_req_keys = a_config["alert_keys"].as_array(); - f_requests_keys.reserve( t_req_keys.size() ); + f_alerts_keys.reserve( t_req_keys.size() ); for( auto t_it = t_req_keys.begin(); t_it != t_req_keys.end(); ++t_it ) { LPROG( dlog, "Monitor <" << f_name << "> will monitor key <" << (*t_it)().as_string() << "> on the alerts exchange" ); @@ -75,7 +78,7 @@ namespace dripline this->cancel( dl_success().rc_value() ); std::this_thread::sleep_for( std::chrono::milliseconds(1100) ); } - if( f_status > status::exchange_declared ) stop(); + if( f_status > status::channel_created ) stop(); } bool monitor::start() @@ -94,24 +97,45 @@ namespace dripline LINFO( dlog, "Connecting to <" << f_address << ":" << f_port << ">" ); - LDEBUG( dlog, "Opening channel for message monitor <" << f_name << ">" ); - f_channel = open_channel(); - if( ! f_channel ) return false; + try + { + open_connection(); + } + catch( connection_error& e ) + { + LERROR( dlog, "Unable to connect to the broker: " << e.what() ); + return false; + } f_status = status::channel_created; - if( ! setup_exchange( f_channel, f_requests_exchange ) ) return false; - if( ! setup_exchange( f_channel, f_alerts_exchange ) ) return false; - f_status = status::exchange_declared; + try + { + // Monitor queue: ephemeral (auto-delete, non-durable); f_name already contains a UUID. + // One queue is bound to both the requests and alerts exchanges via the core helpers. + // All declarations land in core::f_topology so that rmqcpp can redeclare the full + // topology after a connection restart. + f_queue = add_requests_queue( f_name ); - LDEBUG( dlog, "Setting up queue for message monitor <" << f_name << ">" ); - if( ! setup_queue( f_channel, f_name ) ) return false; - f_status = status::queue_declared; + for( const auto& t_key : f_requests_keys ) + { + bind_requests_key( f_name, t_key, f_queue ); + } - if( ! bind_keys() ) return false; - f_status = status::queue_bound; + // Bind the same queue to the alerts exchange for any alert keys. + // add_alerts_*_queue() is not called; we reuse the queue declared above. + for( const auto& t_key : f_alerts_keys ) + { + bind_alerts_key( f_name, t_key, f_queue ); + } - f_consumer_tag = start_consuming( f_channel, f_name ); - if( f_consumer_tag.empty() ) return false; + start_listening( f_vhost, f_requests_ex.f_topology, f_name+"_requests" ); + start_listening( f_vhost, f_alerts_ex.f_topology, f_name+"_alerts" ); + } + catch( connection_error& e ) + { + LERROR( dlog, "Unable to set up monitor topology: " << e.what() ); + return false; + } f_status = status::consuming; return true; @@ -132,18 +156,15 @@ namespace dripline try { - f_receiver_thread = std::thread( &concurrent_receiver::execute, this ); - - if( ! listen_on_queue() ) + // Block until canceled + while( ! is_canceled() ) { - throw dripline_error() << "Something went wrong while listening for messages"; + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); } - - f_receiver_thread.join(); } catch( std::system_error& e ) { - LERROR( dlog, "Could not start the a thread due to a system error: " << e.what() ); + LERROR( dlog, "Could not start a thread due to a system error: " << e.what() ); return false; } catch( dripline_error& e ) @@ -165,94 +186,15 @@ namespace dripline { LINFO( dlog, "Stopping message monitor <" << f_name << ">" ); - if( f_status >= status::listening ) // listening + if( f_status >= status::listening ) { this->cancel( dl_success().rc_value() ); f_status = status::consuming; } - if( f_status >= status::queue_bound ) // queue_bound or consuming - { - if( ! stop_consuming( f_channel, f_consumer_tag ) ) return false; - f_status = status::queue_bound; - } - - if( f_status >= status::queue_declared ) // queue_declared or queue_bound - { - if( ! remove_queue( f_channel, f_name ) ) return false; - f_status = status::exchange_declared; - } - - return true; - } + stop_listening(); - bool monitor::bind_keys() - { - LDEBUG( dlog, "Binding request keys for message monitor <" << f_name << ">" ); - for( auto t_req_key_it = f_requests_keys.begin(); t_req_key_it != f_requests_keys.end(); ++t_req_key_it ) - { - if( ! bind_key( f_channel, f_requests_exchange, f_name, *t_req_key_it ) ) return false; - } - - LDEBUG( dlog, "Binding alerts keys for message monitor <" << f_name << ">" ); - for( auto t_al_key_it = f_alerts_keys.begin(); t_al_key_it != f_alerts_keys.end(); ++t_al_key_it ) - { - if( ! bind_key( f_channel, f_alerts_exchange, f_name, *t_al_key_it ) ) return false; - } - - return true; - } - - bool monitor::listen_on_queue() - { - LINFO( dlog, "Listening for incoming messages on <" << f_name << ">" ); - - while( ! is_canceled() ) - { - amqp_envelope_ptr t_envelope; - core::post_listen_status t_post_listen_status = core::post_listen_status::unknown; - core::listen_for_message( t_envelope, t_post_listen_status, f_channel, f_consumer_tag, f_listen_timeout_ms ); - - if( f_canceled.load() ) - { - LDEBUG( dlog, "Monitor <" << f_name << "> canceled" ); - return true; - } - - if( t_post_listen_status == core::post_listen_status::timeout ) - { - // we end up here every time the listen times out with no message received - continue; - } - - if( t_post_listen_status == core::post_listen_status::soft_error ) - { - LWARN( dlog, "A soft error ocurred while listening for messages for monitor <" << f_name << ">. The channel is still valid" ); - continue; - } - - if( t_post_listen_status == core::post_listen_status::hard_error ) - { - LERROR( dlog, "A hard error ocurred while listening for messages for monitor <" << f_name << ">. The channel is no longer valid" ); - return false; - } - - if( t_post_listen_status == core::post_listen_status::unknown ) - { - LERROR( dlog, "An unknown status occurred while listening for messages for monitor <" << f_name << ">" ); - return false; - } - - // remaining status is core::post_listen_status::message_received - - handle_message_chunk( t_envelope ); - - if( f_canceled.load() ) - { - LDEBUG( dlog, "Monitor <" << f_name << "> canceled" ); - return true; - } - } + f_status = status::nothing; return true; } diff --git a/library/monitor.hh b/library/monitor.hh index 771f8c7e..e4e8f6cd 100644 --- a/library/monitor.hh +++ b/library/monitor.hh @@ -9,15 +9,14 @@ #define DRIPLINE_MONITOR_HH_ #include "core.hh" -#include "listener.hh" -#include "receiver.hh" +#include "message_dispatcher.hh" namespace scarab { class authentication; } -namespace DRIPLINE_API dripline +namespace dripline { /*! @@ -41,20 +40,17 @@ namespace DRIPLINE_API dripline requests keys are bound to the requests exchange. The monitor then waits to receive a message. When a message is seen, it prints it to stdout. */ - class monitor : + class DRIPLINE_API monitor : public core, - public listener_receiver + public message_dispatcher { protected: enum class status { nothing = 0, - channel_created = 10, - exchange_declared = 20, - queue_declared = 30, - queue_bound = 40, - consuming = 50, - listening = 60 + channel_created = 10, ///< broker connection established + consuming = 50, ///< queue declared, keys bound, consumer started + listening = 60 ///< actively waiting for messages }; public: @@ -93,14 +89,7 @@ namespace DRIPLINE_API dripline /// Stops listening for messages and closes the AMQP connection. bool stop(); - protected: - bool bind_keys(); - public: - /// Waits for a single AMQP message and processes it. - /// Returns false if the return is due to an error in this function; returns true otherwise (namely because it was canceled) - virtual bool listen_on_queue(); - /// Handles a single Dripline message by printing it to stdout. /// Printing is done via a prog-level message in the logger. virtual void submit_message( message_ptr_t a_message ); diff --git a/library/receiver.cc b/library/receiver.cc index f7e79714..2ed5a1e6 100644 --- a/library/receiver.cc +++ b/library/receiver.cc @@ -13,7 +13,8 @@ #include "message.hh" #include "logger.hh" -#include "signal_handler.hh" + +#include LOGGER( dlog, "receiver" ); @@ -23,31 +24,31 @@ namespace dripline f_messages(), f_chunks_received(), f_routing_key(), - f_thread(), - f_mutex(), - f_conv(), - f_processing( false ) + f_creation_time(), + f_mutex() {} incoming_message_pack::incoming_message_pack( incoming_message_pack&& a_orig ) : f_messages( std::move(a_orig.f_messages) ), f_chunks_received( a_orig.f_chunks_received ), f_routing_key( std::move(a_orig.f_routing_key) ), - f_thread( std::move(a_orig.f_thread) ), - f_mutex(), - f_conv(), - f_processing( a_orig.f_processing.load() ) + f_creation_time( a_orig.f_creation_time ), + f_mutex() { a_orig.f_chunks_received = 0; - a_orig.f_processing.store( false ); } receiver::receiver() : scarab::cancelable(), f_incoming_messages(), - f_single_message_wait_ms( 1000 ), - f_reply_listen_timeout_ms( 1000 ) + f_single_message_wait_ms( 1000 ) + {} + + receiver::receiver( receiver&& a_orig ) : + scarab::cancelable( std::move(a_orig) ), + f_incoming_messages( std::move(a_orig.f_incoming_messages) ), + f_single_message_wait_ms( a_orig.f_single_message_wait_ms ) {} receiver& receiver::operator=( receiver&& a_orig ) @@ -55,7 +56,6 @@ namespace dripline cancelable::operator=( std::move(a_orig) ); f_incoming_messages = std::move(a_orig.f_incoming_messages); f_single_message_wait_ms = a_orig.f_single_message_wait_ms; - f_reply_listen_timeout_ms = a_orig.f_reply_listen_timeout_ms; return *this; } @@ -63,66 +63,79 @@ namespace dripline { try { - amqp_message_ptr t_message = a_envelope->Message(); - LDEBUG( dlog, "Received a message chunk <" << t_message->MessageId() ); + amqp_message_ptr t_message = bsl::make_shared< BloombergLP::rmqt::Message >( a_envelope->message() ); + LDEBUG( dlog, "Received a message chunk <" << std::string( t_message->messageId() ) << ">" ); + + auto t_parsed = message::parse_message_id( std::string( t_message->messageId() ) ); + const std::string t_message_id( std::get<0>(t_parsed) ); + const unsigned t_chunk_idx = std::get<1>(t_parsed); + const unsigned t_num_chunks = std::get<2>(t_parsed); + + amqp_split_message_ptrs t_complete_messages; + std::string t_complete_routing_key; + bool t_is_complete = false; - auto t_parsed_message_id = message::parse_message_id( t_message->MessageId() ); - std::string t_message_id( std::get<0>(t_parsed_message_id) ); - if( incoming_messages().count( t_message_id ) == 0 ) { - // this path: first chunk for this message - LDEBUG( dlog, "This is the first chunk for this message; creating new message pack" ); - // create the new message_pack object - incoming_message_pack& t_pack = incoming_messages()[t_message_id]; - // set the f_messages vector to the expected size - t_pack.f_messages.resize( std::get<2>(t_parsed_message_id) ); - // put in place the first message chunk received - t_pack.f_messages[std::get<1>(t_parsed_message_id)] = t_message; - t_pack.f_routing_key = a_envelope->RoutingKey(); - t_pack.f_chunks_received = 1; - - if( t_pack.f_messages.size() == 1 ) + std::lock_guard< std::mutex > t_map_lock( f_incoming_messages_mutex ); + + // Lazy stale sweep: evict entries older than f_single_message_wait_ms + auto t_now = std::chrono::steady_clock::now(); + for( auto it = f_incoming_messages.begin(); it != f_incoming_messages.end(); ) { - // if we only expect one chunk, we can bypass creating a separate thread, etc - LDEBUG( dlog, "Single-chunk message being sent directly to processing" ); - process_message_pack( t_pack, t_message_id ); + auto t_age_ms = std::chrono::duration_cast< std::chrono::milliseconds >( t_now - it->second.f_creation_time ).count(); + if( static_cast< unsigned >( t_age_ms ) > f_single_message_wait_ms ) + { + LWARN( dlog, "Evicting stale incomplete message <" << it->first << ">" ); + it = f_incoming_messages.erase( it ); + } + else + { + ++it; + } } - else + + // Find or insert the entry for this message + bool t_is_new = ( f_incoming_messages.find( t_message_id ) == f_incoming_messages.end() ); + incoming_message_pack& t_pack = f_incoming_messages[ t_message_id ]; + if( t_is_new ) { - // start the thread to wait for message chunks - t_pack.f_thread = std::thread([this, &t_pack, &t_parsed_message_id](){ wait_for_message(t_pack, std::get<0>(t_parsed_message_id)); }); - t_pack.f_thread.detach(); + LDEBUG( dlog, "First chunk for message <" << t_message_id << ">; creating new message pack" ); + t_pack.f_messages.resize( t_num_chunks ); + t_pack.f_routing_key = std::string( a_envelope->envelope().routingKey() ); + t_pack.f_creation_time = t_now; + t_pack.f_chunks_received = 0; } - } - else - { - // this path: have already received chunks from this message - LDEBUG( dlog, "This is not the first chunk for this message; adding to message pack" ); - incoming_message_pack& t_pack = incoming_messages()[std::get<0>(t_parsed_message_id)]; - if( t_pack.f_processing.load() ) + + // Store the chunk under per-entry mutex (lock order: map → entry) + std::unique_lock< std::mutex > t_entry_lock( t_pack.f_mutex ); + if( t_pack.f_messages[ t_chunk_idx ] ) { - LWARN( dlog, "Message <" << std::get<0>(t_parsed_message_id) << "> is already being processed\n" << - "Just received chunk " << std::get<1>(t_parsed_message_id) << " of " << std::get<2>(t_parsed_message_id) ); + LWARN( dlog, "Received duplicate chunk " << t_chunk_idx << " for message <" << t_message_id << ">; ignoring" ); } else { - // lock mutex to access f_messages - std::unique_lock< std::mutex > t_lock( t_pack.f_mutex ); - if( t_pack.f_messages[std::get<1>(t_parsed_message_id)] ) - { - LWARN( dlog, "Received duplicate message chunk for message <" << std::get<0>(t_parsed_message_id) << ">; chunk " << std::get<1>(t_parsed_message_id) ); - } - else - { - // add chunk to set of chunks - t_pack.f_messages[std::get<1>(t_parsed_message_id)] = t_message; - ++t_pack.f_chunks_received; - t_lock.unlock(); - // inform the message-processing thread it should check whether it has the complete message - t_pack.f_conv.notify_one(); - } + t_pack.f_messages[ t_chunk_idx ] = t_message; + ++t_pack.f_chunks_received; + LDEBUG( dlog, "Stored chunk " << t_chunk_idx << " for message <" << t_message_id << ">; " + << t_pack.f_chunks_received << " / " << t_pack.f_messages.size() << " received" ); } - } // new/current message if/else block + + if( t_pack.f_chunks_received == t_pack.f_messages.size() ) + { + // Message complete: extract data, release entry lock, then erase from map + t_complete_messages = std::move( t_pack.f_messages ); + t_complete_routing_key = std::move( t_pack.f_routing_key ); + t_entry_lock.unlock(); + f_incoming_messages.erase( t_message_id ); + t_is_complete = true; + LDEBUG( dlog, "Message <" << t_message_id << "> complete; processing" ); + } + } // map lock (and entry lock if not yet released) released here + + if( t_is_complete ) + { + process_message_pack( t_complete_messages, t_complete_routing_key ); + } } catch( dripline_error& e ) { @@ -136,49 +149,11 @@ namespace dripline return; } - void receiver::wait_for_message( incoming_message_pack& a_pack, const std::string& a_message_id ) + void receiver::process_message_pack( amqp_split_message_ptrs& a_messages, const std::string& a_routing_key ) { - std::unique_lock< std::mutex > t_lock( a_pack.f_mutex ); - - LDEBUG( dlog, "Waiting for message; chunks received: " << a_pack.f_chunks_received << " chunks expected: " << a_pack.f_messages.size() ); - - // if the message is already complete, submit it for processing - if( a_pack.f_chunks_received == a_pack.f_messages.size() ) - { - t_lock.release(); // process_message() will unlock the mutex before erasing the message pack - process_message_pack( a_pack, a_message_id ); - return; - } - - auto t_now = std::chrono::system_clock::now(); - while( a_pack.f_conv.wait_until( t_lock, t_now + std::chrono::milliseconds(f_single_message_wait_ms) ) == std::cv_status::no_timeout ) - { - // if the message is complete during the waiting period, submit it for processing - if( a_pack.f_chunks_received == a_pack.f_messages.size() ) - { - t_lock.release(); // process_message() will unlock the mutex before erasing the message pack - process_message_pack( a_pack, a_message_id ); - return; - } - } - - // once the waiting period is over, submit it whether it's complete or not - t_lock.release(); // process_message() will unlock the mutex before erasing the message pack - LWARN( dlog, "Timed out; message may be incomplete" ); - process_message_pack( a_pack, a_message_id ); - - return; - } - - void receiver::process_message_pack( incoming_message_pack& a_pack, const std::string& a_message_id ) - { - a_pack.f_processing.store( true ); try { - message_ptr_t t_message = message::process_message( a_pack.f_messages, a_pack.f_routing_key ); - - a_pack.f_mutex.unlock(); - incoming_messages().erase( a_message_id ); + message_ptr_t t_message = message::process_message( a_messages, a_routing_key ); // if the message is not valid at this point, continue processing it, and we'll deal with it in the endpoint class @@ -205,18 +180,12 @@ namespace dripline reply_ptr_t receiver::wait_for_reply( const sent_msg_pkg_ptr a_receive_reply, int a_timeout_ms ) { - core::post_listen_status t_temp = core::post_listen_status::unknown; - return wait_for_reply( a_receive_reply, t_temp, a_timeout_ms ); - } - - reply_ptr_t receiver::wait_for_reply( const sent_msg_pkg_ptr a_receive_reply, core::post_listen_status& a_status, int a_timeout_ms ) - { - if ( ! a_receive_reply->f_channel ) + if( ! a_receive_reply->f_reply_consumer ) { return reply_ptr_t(); } - if( a_timeout_ms != 0 ) + if( a_timeout_ms != 0 ) { LDEBUG( dlog, "Waiting for a reply (timeout: " << a_timeout_ms << " ms)" ); } @@ -225,228 +194,25 @@ namespace dripline LDEBUG( dlog, "Waiting for a reply (no timeout)" ); } - // Assign the chunk timeout time; it should be f_reply_listen_timeout_ms unless a_timeout_ms is shorter than f_reply_listen_timeout_ms - unsigned t_chunk_timeout_ms = f_reply_listen_timeout_ms; - if( a_timeout_ms > 0 && a_timeout_ms < t_chunk_timeout_ms ) - { - t_chunk_timeout_ms = a_timeout_ms; - } - - // for checking the wait_for_reply timeout - auto t_timeout_time = std::chrono::system_clock::now() + std::chrono::milliseconds(a_timeout_ms); + auto t_future = a_receive_reply->f_reply_promise->get_future(); + auto t_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( a_timeout_ms ); - // wait for messages until either: - // 1. the channel is no longer valid (return empty reply pointer; a_chan_valid will be false) - // 2. listening times out (return empty reply pointer; a_chan_valid will be true) - // 3. a full dripline message is received (return message) - // 4. error processing a recieved amqp message (return empty reply pointer) - while( ! is_canceled() && (a_timeout_ms == 0 || std::chrono::system_clock::now() < t_timeout_time) ) + while( ! is_canceled() ) { - amqp_envelope_ptr t_envelope; - core::listen_for_message( t_envelope, a_status, a_receive_reply->f_channel, a_receive_reply->f_consumer_tag, t_chunk_timeout_ms, false ); - - // check whether we canceled while listening - if( is_canceled() ) + auto t_status = t_future.wait_for( std::chrono::milliseconds( 100 ) ); + if( t_status == std::future_status::ready ) { - LDEBUG( dlog, "Receiver was canceled before receiving reply" ); - return reply_ptr_t(); + return t_future.get(); } - - // there was a soft error listening on the channel; no message received - if( a_status == core::post_listen_status::soft_error ) + if( a_timeout_ms > 0 && std::chrono::steady_clock::now() >= t_deadline ) { - LWARN( dlog, "There was a soft error while listening for a reply; no message received" ); - continue; - } - - // there was a hard error listening on the channel; no message received - if( a_status == core::post_listen_status::hard_error ) - { - LERROR( dlog, "There was a hard error error while listening for a reply; no message received" ); - return reply_ptr_t(); - } - - // listening timed out - if( a_status == core::post_listen_status::timeout ) - { - LTRACE( dlog, "Listening for reply message chunks timed out" ); - // continue to wait for message chunks - continue; - } - - // unknown state; should not get here - if( a_status == core::post_listen_status::unknown ) - { - LERROR( dlog, "An unknown status occurred while listening for messages" ); + LWARN( dlog, "Timed out waiting for reply" ); return reply_ptr_t(); } - - // because core::listen_for_message uses whether or not the envelope is empty to differentiate between a received message and a timeout, - // we will never have an empty envelope and a status == message_received - - // go ahead with message processing - try - { - amqp_message_ptr t_message = t_envelope->Message(); - LDEBUG( dlog, "Received a message chunk <" << t_message->MessageId() ); - - auto t_parsed_message_id = message::parse_message_id( t_message->MessageId() ); - if( f_incoming_messages.count( std::get<0>(t_parsed_message_id) ) == 0 ) - { - // this path: first chunk for this message - LDEBUG( dlog, "This is the first chunk for this message; creating new message pack" ); - // create the new message_pack object - incoming_message_pack& t_pack = f_incoming_messages[std::get<0>(t_parsed_message_id)]; - // set the f_messages vector to the expected size - t_pack.f_messages.resize( std::get<2>(t_parsed_message_id) ); - // put in place the first message chunk received - t_pack.f_messages[std::get<1>(t_parsed_message_id)] = t_message; - t_pack.f_routing_key = t_envelope->RoutingKey(); - t_pack.f_chunks_received = 1; - - if( t_pack.f_chunks_received == t_pack.f_messages.size() ) - { - return process_received_reply( t_pack, std::get<0>(t_parsed_message_id) ); - } - // else, need more chunks - } - else - { - // this path: have already received chunks from this message - LDEBUG( dlog, "This is not the first chunk for this message; adding to message pack" ); - incoming_message_pack& t_pack = f_incoming_messages[std::get<0>(t_parsed_message_id)]; - if( t_pack.f_processing.load() ) - { - LWARN( dlog, "Message <" << std::get<0>(t_parsed_message_id) << "> is already being processed\n" << - "Just received chunk " << std::get<1>(t_parsed_message_id) << " of " << std::get<2>(t_parsed_message_id) ); - } - else - { - if( t_pack.f_messages[std::get<1>(t_parsed_message_id)] ) - { - LWARN( dlog, "Received duplicate message chunk for message <" << std::get<0>(t_parsed_message_id) << ">; chunk " << std::get<1>(t_parsed_message_id) ); - } - else - { - // add chunk to set of chunks - t_pack.f_messages[std::get<1>(t_parsed_message_id)] = t_message; - ++t_pack.f_chunks_received; - if( t_pack.f_chunks_received == t_pack.f_messages.size() ) - { - return process_received_reply( t_pack, std::get<0>(t_parsed_message_id) ); - } - } - } - } - - } - catch( dripline_error& e ) - { - LERROR( dlog, "There was a problem processing the message: " << e.what() ); - a_status = core::post_listen_status::soft_error; - return reply_ptr_t(); - } - - } // end while( ! is_canceled() && not timed out ) - - // check if listening timed out - if( ! is_canceled() && std::chrono::system_clock::now() > t_timeout_time ) - { - LINFO( dlog, "Listening for reply message timed out" ); - a_status = core::post_listen_status::timeout; - return reply_ptr_t(); - } - - LDEBUG( dlog, "Receiver was canceled" ); - return reply_ptr_t(); - } - - reply_ptr_t receiver::process_received_reply( incoming_message_pack& a_pack, const std::string& a_message_id ) - { - a_pack.f_processing.store( true ); - try - { - message_ptr_t t_message = message::process_message( a_pack.f_messages, a_pack.f_routing_key ); - - f_incoming_messages.erase( a_message_id ); - - if( t_message->is_reply() ) - { - return std::static_pointer_cast< msg_reply >( t_message ); - } - else - { - throw dripline_error() << "Non-reply message received"; - } - } - catch( dripline_error& e ) - { - LERROR( dlog, "Dripline exception caught while handling message: " << e.what() ); - } - catch( amqp_exception& e ) - { - LERROR( dlog, "AMQP exception caught while sending reply: (" << e.reply_code() << ") " << e.reply_text() ); - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "AMQP Library Exception caught while sending reply: (" << e.ErrorCode() << ") " << e.what() ); - } - catch( std::exception& e ) - { - LERROR( dlog, "Standard exception caught while sending reply: " << e.what() ); } + LDEBUG( dlog, "Receiver canceled while waiting for reply" ); return reply_ptr_t(); - - } - - concurrent_receiver::concurrent_receiver() : - receiver(), - f_message_queue() - {} - - concurrent_receiver::concurrent_receiver( concurrent_receiver&& a_orig ) : - receiver( std::move(a_orig) ), - f_message_queue() - {} - - concurrent_receiver::~concurrent_receiver() - {} - - concurrent_receiver& concurrent_receiver::operator=( concurrent_receiver&& a_orig ) - { - receiver::operator=( std::move(a_orig) ); - // nothing to do with message queue - return *this; } - void concurrent_receiver::process_message( message_ptr_t a_message ) - { - f_message_queue.push( a_message ); - return; - } - - void concurrent_receiver::execute() - { - try - { - while( ! is_canceled() ) - { - message_ptr_t t_message; - if( f_message_queue.timed_wait_and_pop( t_message ) ) - { - this->submit_message( t_message ); - } - } - } - catch( const std::exception& e ) - { - // shutdown gracefully on an exception - LERROR( dlog, "Exception caught; shutting down.\n" << "\t" << e.what() ); - scarab::signal_handler::cancel_all( RETURN_ERROR ); - } - } - - - } /* namespace dripline */ diff --git a/library/receiver.hh b/library/receiver.hh index 5021cdea..8d58bc2f 100644 --- a/library/receiver.hh +++ b/library/receiver.hh @@ -13,13 +13,11 @@ #include "dripline_fwd.hh" #include "cancelable.hh" -#include "concurrent_queue.hh" #include "member_variables.hh" -#include -#include +#include #include -#include +#include namespace dripline { @@ -34,10 +32,8 @@ namespace dripline amqp_split_message_ptrs f_messages; unsigned f_chunks_received; std::string f_routing_key; - std::thread f_thread; + std::chrono::steady_clock::time_point f_creation_time; std::mutex f_mutex; - std::condition_variable f_conv; - std::atomic< bool > f_processing; incoming_message_pack(); incoming_message_pack( const incoming_message_pack& ) = delete; incoming_message_pack( incoming_message_pack&& a_orig ); @@ -53,33 +49,33 @@ namespace dripline @brief A receiver is able to collect Dripline message chunks and reassemble them into a complete Dripline message. @details - This is a mix-in class for synchronously receiving and processing Dripline messages. + This is a mix-in class for receiving and processing Dripline messages. - Dripline messages can be broken up into multiple chunks, each of which is transported as an AMQP message. - A receiver is responsible for handling message chunks, storing incomplete Dripline messages, and eventually + Dripline messages can be broken up into multiple chunks, each of which is transported as an AMQP message. + A receiver is responsible for handling message chunks, storing incomplete Dripline messages, and eventually processing complete Dripline messages. The receiver class contains an interface specifically for users waiting to receive reply messages: `wait_for_reply()`. - When the first message chunk for a message is received, one of two things happens: - 1. if the message comprises one chunk, then the message is processed immediately; - 2. if the message comprises multiple chunks, then a separate thread is spun up to wait for the remaining chunks. + When a message chunk arrives via `handle_message_chunk()`, it is stored in the incoming-message map. + Message chunks for a given message can be received in any order. Once all chunks for a message have + arrived, `process_message_pack()` is called inline (no separate thread is spawned). - Incomplete messages are stored in the incoming-message map. Message chunks for a given message can be received - in any order. The receiver will wait `single_message_wait_ms` ms for all of the chunks of a message to arrive - before timing out processing the incomplete message. + Stale incomplete messages (entries older than `single_message_wait_ms` ms) are lazily evicted at the + start of each `handle_message_chunk()` call. The actual assembly of message chunks into complete messages is done in @ref message. - The `receiver` class itself does not know how to process a message. This must be implemented by the class derived from `receiver`. - The default implementation of `process_message()` will throw a `dripline_error`. + The `receiver` class itself does not know how to process a message. This must be implemented by the + class derived from `receiver`. The default implementation of `process_message()` will throw a + `dripline_error`. */ class DRIPLINE_API receiver : public virtual scarab::cancelable { public: receiver(); receiver( const receiver& a_orig ) = delete; - receiver( receiver&& a_orig ) = default; + receiver( receiver&& a_orig ); virtual ~receiver() = default; receiver& operator=( const receiver& a_orig ) = delete; @@ -91,11 +87,8 @@ namespace dripline /// For single-chunk messages, processes the message immediately. void handle_message_chunk( amqp_envelope_ptr a_envelope ); - /// Waits for messages for a set amount of time (`single_message_wait_ms`), and submits the message pack for processing. - /// Intended to be used in a separate thread for each message pack. - void wait_for_message( incoming_message_pack& a_pack, const std::string& a_message_id ); /// Converts a message pack into a Dripline message, and then submits the message for processing. - void process_message_pack( incoming_message_pack& a_pack, const std::string& a_message_id ); + void process_message_pack( amqp_split_message_ptrs& a_messages, const std::string& a_routing_key ); /// Processes a single Dripline message. /// This is the default implementation that always throws a `dripline_error`. @@ -105,8 +98,6 @@ namespace dripline mv_referrable( incoming_message_map, incoming_messages ); /// Wait time for all message chunks from a single dripline message mv_accessible( unsigned, single_message_wait_ms ); - /// Listen timeout for individual message chunks when waiting for replies - mv_accessible( unsigned, reply_listen_timeout_ms ); public: @@ -118,67 +109,13 @@ namespace dripline @return Reply message */ reply_ptr_t wait_for_reply( const sent_msg_pkg_ptr a_receive_reply, int a_timeout_ms = 0 ); - /*! - User interface for waiting for a reply message. - This can be called multiple times to receive multiple replies. - @param[in] a_receive_reply The sent-message package from the request. - @param[in] a_timeout_ms Timeout for waiting for a reply; if it's 0, there will be no timeout. - @param[out] a_chan_valid Returns whether or not the channel is still valid for use after receiving a message. - @return Reply message - */ - reply_ptr_t wait_for_reply( const sent_msg_pkg_ptr a_receive_reply, core::post_listen_status& a_status, int a_timeout_ms = 0 ); protected: - reply_ptr_t process_received_reply( incoming_message_pack& a_pack, const std::string& a_message_id ); - - }; - - /*! - @class concurrent_receiver - @author N.S. Oblath - - @brief Receives and processes messages concurrently - - @details - This class enables Dripline messages to be received and processed concurrently. It is intended to be used in - conjuction with a listener, as in @ref listener_receiver. + // (no protected helpers currently) - The typical use case involves three threads: - 1. A listener gets messages from the AMQP channel (using `listen_on_queue(), e.g. @ref service or @ref endpoint_listener_receiver) and - calls `receiver::handle_message_chunk()` - 2. A receiver has a timing thread waiting for multiple message chunks (if relevant); - when the message is complete, `concurrent_receiver::process_message()` is called, which deposits the message in a concurrent queue. - 3. A concurrent_receiver picks up the complete message from the concurrent queue, and processes the message using `submit_message()`. - - The `execute()` function implements thread 3. - - A class deriving from concurrent_receiver must implement `submit_message()`. - */ - class DRIPLINE_API concurrent_receiver : public receiver - { - public: - concurrent_receiver(); - concurrent_receiver( const concurrent_receiver& ) = delete; - concurrent_receiver( concurrent_receiver&& a_orig ); - virtual ~concurrent_receiver(); - - concurrent_receiver& operator=( const concurrent_receiver& ) = delete; - concurrent_receiver& operator=( concurrent_receiver&& a_orig ); - - public: - /// Deposits the message in the concurrent queue (called by the listener) - virtual void process_message( message_ptr_t a_message ); - - /// Handles messages that appear in the concurrent queue by calling `submit_message()`. - void execute(); - - protected: - /// Handles messages according to the use case. It's to be implemented by the class inheriting from concurrent_receiver - /// For a concrete example, see @ref service or @ref endpoint_listener_receiver. - virtual void submit_message( message_ptr_t a_message ) = 0; + private: + mutable std::mutex f_incoming_messages_mutex; - mv_referrable( scarab::concurrent_queue< message_ptr_t >, message_queue ); - mv_referrable( std::thread, receiver_thread ); }; } /* namespace dripline */ diff --git a/library/relayer.cc b/library/relayer.cc index c5fc0509..5ec72a9d 100644 --- a/library/relayer.cc +++ b/library/relayer.cc @@ -129,12 +129,6 @@ namespace dripline } reply_ptr_t relayer::wait_for_reply( const wait_for_send_pkg_ptr a_receive_reply, int a_timeout_ms ) - { - core::post_listen_status t_temp = core::post_listen_status::unknown; - return wait_for_reply( a_receive_reply, t_temp, a_timeout_ms ); - } - - reply_ptr_t relayer::wait_for_reply( const wait_for_send_pkg_ptr a_receive_reply, core::post_listen_status& a_status, int a_timeout_ms ) { std::unique_lock< std::mutex > t_lock( a_receive_reply->f_mutex ); auto t_deadline = std::chrono::system_clock::now() + std::chrono::milliseconds( a_timeout_ms ); @@ -147,7 +141,7 @@ namespace dripline return reply_ptr_t(); } } - return f_msg_receiver.wait_for_reply( a_receive_reply->f_sent_msg_pkg_ptr, a_status, a_timeout_ms ); + return f_msg_receiver.wait_for_reply( a_receive_reply->f_sent_msg_pkg_ptr, a_timeout_ms ); } } diff --git a/library/relayer.hh b/library/relayer.hh index cb57f3fb..27593cf9 100644 --- a/library/relayer.hh +++ b/library/relayer.hh @@ -86,15 +86,6 @@ namespace dripline @return Reply message */ reply_ptr_t wait_for_reply( const wait_for_send_pkg_ptr a_receive_reply, int a_timeout_ms = 0 ); - /*! - User interface for waiting for a reply message. - This can be called multiple times to receive multiple replies. - @param[in] a_receive_reply The sent-message package from the request. - @param[in] a_timeout_ms Timeout for waiting for a reply; if it's 0, there will be no timeout. - @param[out] a_status Returns the output status after receiving a message (or failing to do so). - @return Reply message - */ - reply_ptr_t wait_for_reply( const wait_for_send_pkg_ptr a_receive_reply, core::post_listen_status& a_status, int a_timeout_ms = 0 ); private: void do_cancellation( int a_code ); diff --git a/library/service.cc b/library/service.cc index d450b9b2..3016372b 100644 --- a/library/service.cc +++ b/library/service.cc @@ -16,6 +16,8 @@ #include "authentication.hh" #include "logger.hh" +#include "rmqt_queue.h" + using scarab::authentication; using scarab::param_node; using scarab::param_value; @@ -33,7 +35,7 @@ namespace dripline core( a_config.has("dripline_mesh") ? a_config["dripline_mesh"].as_node() : dripline_config(), a_auth, a_make_connection ), endpoint( a_config.get_value( "name", "dlcpp_service" ) ), - listener_receiver(), + message_dispatcher(), heartbeater( this ), scheduler<>(), f_auth( a_auth ), @@ -46,33 +48,16 @@ namespace dripline f_broadcast_key( a_config.get_value( "broadcast_key", "broadcast" ) ) { LDEBUG( dlog, "Service (cpp) created with config:\n" << a_config ); + // setup endpoint's f_service pointer + endpoint::f_service = this; + heartbeater::f_service = this; // get more values from the config - // default of f_listen_timeout_ms is in the listener class - f_listen_timeout_ms = a_config.get_value( "loop_timeout_ms", f_listen_timeout_ms ); - heartbeater::f_check_timeout_ms = f_listen_timeout_ms; + heartbeater::f_check_timeout_ms = a_config.get_value( "loop_timeout_ms", 1000 ); // default of f_single_message_wait_ms is in the receiver class f_single_message_wait_ms = a_config.get_value( "message_wait_ms", f_single_message_wait_ms ); // default of f_heartbeat_interval_s is in the heartbeater class f_heartbeat_interval_s = a_config.get_value( "heartbeat_interval_s", f_heartbeat_interval_s ); } -/* - service::service( const bool a_make_connection, const scarab::param_node& a_config, const scarab::authentication& a_auth ) : - scarab::cancelable(), - core( a_make_connection, a_config ), - endpoint( "" ), - listener_receiver(), - heartbeater(), - scheduler<>(), - std::enable_shared_from_this< service >(), - f_status( status::nothing ), - f_enable_scheduling( a_config.get_value("enable-scheduling", false ) ), - f_id( generate_random_uuid() ), - f_sync_children(), - f_async_children(), - f_broadcast_key() - { - } -*/ service::~service() { @@ -81,7 +66,7 @@ namespace dripline this->cancel( dl_success().rc_value() ); std::this_thread::sleep_for( std::chrono::milliseconds(1100) ); } - if( f_status > status::exchange_declared ) stop(); + if( f_status > status::nothing ) stop(); } service& service::operator=( service&& a_orig ) @@ -89,7 +74,7 @@ namespace dripline cancelable::operator=( std::move(a_orig) ); core::operator=( std::move(a_orig) ); endpoint::operator=( std::move(a_orig)); - listener_receiver::operator=( std::move(a_orig) ); + message_dispatcher::operator=( std::move(a_orig) ); heartbeater::operator=( std::move(a_orig) ); scheduler<>::operator=( std::move(a_orig) ); @@ -121,12 +106,12 @@ namespace dripline bool service::add_async_child( endpoint_ptr_t a_endpoint_ptr ) { - lr_ptr_t t_listener_receiver_ptr = std::dynamic_pointer_cast< listener_receiver >( a_endpoint_ptr ); - if( ! t_listener_receiver_ptr ) + elr_ptr_t t_elr_ptr = std::dynamic_pointer_cast< endpoint_listener_receiver >( a_endpoint_ptr ); + if( ! t_elr_ptr ) { - t_listener_receiver_ptr.reset( new endpoint_listener_receiver( a_endpoint_ptr ) ); + t_elr_ptr = std::make_shared< endpoint_listener_receiver >( a_endpoint_ptr ); } - auto t_inserted = f_async_children.insert( std::make_pair( a_endpoint_ptr->name(), t_listener_receiver_ptr ) ); + auto t_inserted = f_async_children.insert( std::make_pair( a_endpoint_ptr->name(), t_elr_ptr ) ); if( t_inserted.second ) { a_endpoint_ptr->set_service( this ); @@ -143,7 +128,7 @@ namespace dripline { unsigned n_failures = 0; bool t_do_repeat = true; // start true so that we get into the repeat loop - // Repeat loop for listening: we may call to listen_on_queue() multiple times + // Repeat loop for restarting on connection failure while( t_do_repeat ) { t_do_repeat = false; // set false because we'll only do the repeat based on the conditions below @@ -191,11 +176,6 @@ namespace dripline bool service::start() { - if( ! f_make_connection ) - { - LWARN( dlog, "Should not start service when make_connection is disabled" ); - return true; - } if( f_name.empty() ) { LERROR( dlog, "Service requires a queue name to be started" ); @@ -206,23 +186,43 @@ namespace dripline endpoint::f_service = this; heartbeater::f_service = this; - LINFO( dlog, "Connecting to <" << f_address << ":" << f_port << ">" ); + if( ! f_make_connection ) + { + LWARN( dlog, "Make-connection is disabled" ); + } + else + { + LINFO( dlog, "Connecting to <" << f_address << ":" << f_port << ">" ); - if( ! open_channels() ) return false; - f_status = status::channel_created; + try + { + open_channels(); + f_status = status::connected; - if( ! setup_exchange( f_channel, f_requests_exchange ) ) return false; - if( ! setup_exchange( f_channel, f_alerts_exchange ) ) return false; - f_status = status::exchange_declared; + add_queues(); + f_status = status::topology_set; - if( ! setup_queues() ) return false; - f_status = status::queue_declared; + bind_keys(); + f_status = status::queues_bound; - if( ! bind_keys() ) return false; - f_status = status::queue_bound; + } + catch( connection_error& e ) + { + LERROR( dlog, "Error while starting AMQP connection: " << e.what() ); + return false; + } + } - if( ! start_consuming() ) return false; - f_status = status::consuming; + try + { + start_threads(); + f_status = status::threads_started; + } + catch(const dripline_error& e) + { + LERROR( dlog, "Error while starting threads: " << e.what() ); + return false; + } return true; } @@ -235,86 +235,40 @@ namespace dripline return true; } - f_status = status::listening; - try { - if( f_heartbeat_interval_s != 0 ) - { - LINFO( dlog, "Starting heartbeat" ); - f_heartbeat_thread = std::thread( &heartbeater::execute, this, f_name, f_id, f_heartbeat_routing_key ); - } - else - { - LINFO( dlog, "Heartbeat disabled" ); - } + // Pass the shared topology (owned by core) so that rmqcpp can redeclare + // exchanges, queues, and bindings after a connection restart. + start_listening( f_vhost, f_requests_ex.f_topology, f_name+"_requests" ); + start_listening( f_vhost, f_alerts_ex.f_topology, f_name+"_alerts" ); - if( f_enable_scheduling ) - { - LINFO( dlog, "Starting scheduler" ); - f_scheduler_thread = std::thread( &scheduler::execute, this ); - } - else + // Each async child gets its own ephemeral queue; they all share the same + // core topology which already contains all queue and binding declarations. + for( auto& t_child_pair : f_async_children ) { - LINFO( dlog, "Scheduler disabled" ); - } - - LINFO( dlog, "Starting receiver thread" ); - f_receiver_thread = std::thread( &concurrent_receiver::execute, this ); - - // lambda to cancel everything on an error from listener::listen_on_queue() - bool t_listen_error = false; - auto t_cancel_on_listen_error = [&t_listen_error, this](listener& a_listener) { - if( ! a_listener.listen_on_queue() ) - { - t_listen_error = true; - this->cancel( RETURN_ERROR ); - } - }; - - if( ! f_async_children.empty() ) { LINFO( dlog, "Starting async children" ); } - else { LDEBUG( dlog, "No async children to start" ); } - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) - { - t_child_it->second->receiver_thread() = std::thread( &concurrent_receiver::execute, static_cast< listener_receiver* >(t_child_it->second.get()) ); - t_child_it->second->listener_thread() = std::thread( t_cancel_on_listen_error, std::ref(*t_child_it->second.get()) ); + const std::string& t_child_name = t_child_pair.first; + t_child_pair.second->start_listening( f_vhost, f_requests_ex.f_topology, t_child_name+"_requests" ); + t_child_pair.second->start_listening( f_vhost, f_alerts_ex.f_topology, t_child_name+"_alerts" ); } + } + catch( connection_error& e ) + { + LERROR( dlog, "Unable to set up service topology: " << e.what() ); + return false; + } - LINFO( dlog, "Starting listener thread" ); - t_cancel_on_listen_error( *this ); + f_status = status::listening; - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) + try + { + // Block until canceled + while( ! is_canceled() ) { - t_child_it->second->listener_thread().join(); - t_child_it->second->receiver_thread().join(); + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); } - f_receiver_thread.join(); + stop_threads(); - if( f_heartbeat_thread.joinable() ) - { - f_heartbeat_thread.join(); - } - if( f_scheduler_thread.joinable() ) - { - f_scheduler_thread.join(); - } - - if( t_listen_error) throw dripline_error() << "Something went wrong while listening for messages"; - } - catch( std::system_error& e ) - { - LERROR( dlog, "Could not start the a thread due to a system error: " << e.what() ); - return false; - } - catch( dripline_error& e ) - { - LERROR( dlog, "Dripline error while running service: " << e.what() ); - return false; } catch( std::exception& e ) { @@ -329,72 +283,62 @@ namespace dripline { LINFO( dlog, "Stopping service on <" << f_name << ">" ); - if( f_status >= status::listening ) // listening or processing + if( f_status >= status::listening ) { this->cancel( dl_success().rc_value() ); - f_status = status::consuming; - } - if( f_status >= status::queue_bound ) // queue_bound or consuming - { - if( ! stop_consuming() ) return false; - f_status = status::queue_bound; } + stop_listening(); - if( f_status >= status::queue_declared ) // queue_declared or queue_bound + for( auto& t_child_pair : f_async_children ) { - if( ! remove_queue() ) return false; - f_status = status::exchange_declared; + t_child_pair.second->stop_listening(); } + stop_threads(); + + f_status = status::nothing; return true; } - bool service::open_channels() + void service::open_channels() { - LDEBUG( dlog, "Opening channel for service <" << f_name << ">" ); - f_channel = open_channel(); - if( ! f_channel ) return false; - - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) - { - LDEBUG( dlog, "Opening channel for child <" << t_child_it->first << ">" ); - t_child_it->second->channel() = open_channel(); - t_child_it->second->set_listen_timeout_ms( f_listen_timeout_ms ); - } - return true; + open_connection(); + return; } - bool service::setup_queues() + void service::add_queues() { - LDEBUG( dlog, "Setting up queue for service <" << f_name << ">" ); - if( ! setup_queue( f_channel, f_name ) ) return false; + // Service's own queue + // Default properties are used: auto-delete=off, durable=on, exclusive=off, single-active-consumer=on + LDEBUG( dlog, "Adding ephemeral queue for service <" << f_name << ">" ); + f_queue = add_requests_queue( f_name ); + // Queues for asynchronous endpoints + // Default properties are used: auto-delete=off, durable=on, exclusive=off, single-active-consumer=on for( async_map_t::iterator t_child_it = f_async_children.begin(); t_child_it != f_async_children.end(); ++t_child_it ) { - LDEBUG( dlog, "Setting up queue for async child <" << t_child_it->first << ">" ); - if( ! setup_queue( t_child_it->second->channel(), t_child_it->first ) ) return false; + LDEBUG( dlog, "Adding ephemeral queue for async child <" << t_child_it->first << ">" ); + t_child_it->second->set_queue( add_requests_queue( t_child_it->first ) ); } - return true; + return; } - bool service::bind_keys() + void service::bind_keys() { LDEBUG( dlog, "Binding primary service keys" ); - if( ! bind_key( f_channel, f_requests_exchange, f_name, f_name + ".#" ) ) return false; - if( ! bind_key( f_channel, f_requests_exchange, f_name, f_broadcast_key + ".#" ) ) return false; + bind_requests_key( f_name, f_name + ".#", f_queue ); + bind_requests_key( f_name, f_broadcast_key + ".#", f_queue ); LDEBUG( dlog, "Binding keys for synchronous children" ); for( sync_map_t::const_iterator t_child_it = f_sync_children.begin(); t_child_it != f_sync_children.end(); ++t_child_it ) { - if( ! bind_key( f_channel, f_requests_exchange, f_name, t_child_it->first + ".#" ) ) return false; + bind_requests_key( f_name, t_child_it->first + ".#", f_queue ); } LDEBUG( dlog, "Binding keys for asynchronous children" ); @@ -402,110 +346,68 @@ namespace dripline t_child_it != f_async_children.end(); ++t_child_it ) { - if( ! bind_key( t_child_it->second->channel(), f_requests_exchange, t_child_it->first, t_child_it->first + ".#" ) ) return false; + bind_requests_key( t_child_it->first, t_child_it->first + ".#", t_child_it->second->get_queue() ); } - return true; - } - - bool service::start_consuming() - { - f_consumer_tag = core::start_consuming( f_channel, f_name ); - if( f_consumer_tag.empty() ) return false; + // All queue and binding declarations are accumulated in core::f_topology. + // No per-dispatcher topology copies are needed; start_listening() will pass + // f_topology (from core) directly to rmqcpp. - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) - { - t_child_it->second->consumer_tag() = core::start_consuming( t_child_it->second->channel(), t_child_it->first ); - if( t_child_it->second->consumer_tag().empty() ) return false; - } - return true; - } - - bool service::stop_consuming() - { - // doesn't stop on failure; continues trying to stop consuming - bool t_success = true; - t_success = core::stop_consuming( f_channel, f_consumer_tag ); - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) - { - t_success = core::stop_consuming( t_child_it->second->channel(), t_child_it->second->consumer_tag() ); - } - return t_success; - } - - bool service::remove_queue() - { - // doesn't stop on failure; continues trying to remove queues - bool t_success = true; - t_success = core::remove_queue( f_channel, f_name ); - for( async_map_t::iterator t_child_it = f_async_children.begin(); - t_child_it != f_async_children.end(); - ++t_child_it ) - { - t_success = core::remove_queue( t_child_it->second->channel(), t_child_it->first ); - } - return t_success; + return; } - bool service::listen_on_queue() + void service::start_threads() { - LINFO( dlog, "Listening for incoming messages on <" << f_name << ">" ); - - while( ! is_canceled() ) + try { - amqp_envelope_ptr t_envelope; - core::post_listen_status t_post_listen_status = core::post_listen_status::unknown; - core::listen_for_message( t_envelope, t_post_listen_status, f_channel, f_consumer_tag, f_listen_timeout_ms ); - - if( f_canceled.load() ) + if( f_heartbeat_interval_s != 0 ) { - LDEBUG( dlog, "Service canceled" ); - return true; + LINFO( dlog, "Starting heartbeat" ); + f_heartbeat_thread = std::thread( &heartbeater::execute, this, f_name, f_id, f_heartbeat_routing_key ); } - - if( t_post_listen_status == core::post_listen_status::timeout ) + else { - // we end up here every time the listen times out with no message received - continue; + LINFO( dlog, "Heartbeat disabled" ); } - if( t_post_listen_status == core::post_listen_status::soft_error ) + if( f_enable_scheduling ) { - LWARN( dlog, "A soft error ocurred while listening for messages for <" << f_name << ">. The channel is still valid" ); - continue; + LINFO( dlog, "Starting scheduler" ); + f_scheduler_thread = std::thread( &scheduler::execute, this ); } - - if( t_post_listen_status == core::post_listen_status::hard_error ) + else { - LERROR( dlog, "A hard error ocurred while listening for messages for <" << f_name << ">. The channel is no longer valid" ); - return false; + LINFO( dlog, "Scheduler disabled" ); } + } + catch( std::system_error& e ) + { + throw dripline_error() << "Could not start threads due to a system error: " << e.what(); + } + catch( dripline_error& e ) + { + throw dripline_error() << "Dripline error while starting threads: " << e.what(); + } + catch( std::exception& e ) + { + throw dripline_error() << "Error while starting threads: " << e.what(); + } - if( t_post_listen_status == core::post_listen_status::unknown ) + return; + } + + void service::stop_threads() + { + if( f_heartbeat_thread.joinable() ) { - LERROR( dlog, "An unknown status occurred while listening for messages for <" << f_name << ">" ); - return false; + f_heartbeat_thread.join(); } - - // remaining status is core::post_listen_status::message_received - - f_status = status::processing; - - handle_message_chunk( t_envelope ); - - if( f_canceled.load() ) + if( f_scheduler_thread.joinable() ) { - LDEBUG( dlog, "Service <" << f_name << "> canceled" ); - return true; + f_scheduler_thread.join(); } - f_status = status::listening; - } - return true; + return; } void service::submit_message( message_ptr_t a_message ) @@ -520,16 +422,6 @@ namespace dripline LERROR( dlog, "<" << f_name << "> Dripline exception caught while handling message: " << e.what() ); throw; } - catch( amqp_exception& e ) - { - LERROR( dlog, "<" << f_name << "> AMQP exception caught while handling message: (" << e.reply_code() << ") " << e.reply_text() ); - throw; - } - catch( amqp_lib_exception& e ) - { - LERROR( dlog, "<" << f_name << "> AMQP Library Exception caught while handling message: (" << e.ErrorCode() << ") " << e.what() ); - throw; - } catch( std::exception& e ) { LERROR( dlog, "<" << f_name << "> Standard exception caught while handling message: " << e.what() ); @@ -539,20 +431,6 @@ namespace dripline return; } - void service::send_reply( reply_ptr_t a_reply ) const - { - LDEBUG( dlog, "Sending reply message to <" << a_reply->routing_key() << ">:\n" << - " Return code: " << a_reply->get_return_code() << '\n' << - " Return message: " << a_reply->return_message() << '\n' << - " Payload:\n" << a_reply->payload() ); - - if( ! send( a_reply ) ) - { - LWARN( dlog, "Something went wrong while sending the reply" ); - } - return; - } - reply_ptr_t service::on_request_message( request_ptr_t a_request ) { std::string t_first_token( a_request->routing_key() ); diff --git a/library/service.hh b/library/service.hh index b0a31f78..9d5bf64b 100644 --- a/library/service.hh +++ b/library/service.hh @@ -11,9 +11,8 @@ #include "core.hh" #include "endpoint.hh" #include "heartbeater.hh" +#include "message_dispatcher.hh" #include "scheduler.hh" -#include "listener.hh" -#include "receiver.hh" #include "dripline_exceptions.hh" #include "service_config.hh" @@ -57,21 +56,14 @@ namespace dripline A service has a number of key characteristics (most of which come from its parent classes): * `core` -- Has all of the basic AMQP capabilities, sending messages, and making and manipulating connections * `endpoint` -- Handles Dripline messages - * `listener_receiver` -- Asynchronously recieves AMQP messages and turns them into Dripline messages + * `message_dispatcher` -- Receives AMQP messages via rmqcpp callbacks and dispatches them as Dripline messages * `heartbeater` -- Sends periodic heartbeat messages * `scheduler` -- Can schedule events - As is apparent from the above descriptions, a service is responsible for a number of threads - when it executes: - * Listening -- grabs AMQP messages off the channel when they arrive - * Message-wait -- any incomplete multi-part Dripline message will setup a thread to wait - * until the message is complete, and then submits it for handling - * Receiver -- grabs completed Dripline messages and handles it - * Async endpoint listening -- same as abovefor each asynchronous endpoint - * Async endpoint message-wait -- same as above for each asynchronous endpoint - * Async endpoint receiver -- same as above for each asynchronous endpoint - * Heatbeater -- sends regular heartbeat messages - * Scheduler -- executes scheduled events + Message delivery is handled by rmqcpp's internal thread pool via callbacks; dripline-cpp + manages only the following threads: + * Heartbeater -- sends regular heartbeat messages + * Scheduler -- executes scheduled events In addition to receiving messages from the broker, a user or client code can give messages directly to the service using `process_message(message)`. @@ -79,7 +71,7 @@ namespace dripline class DRIPLINE_API service : public core, public endpoint, - public listener_receiver, + public message_dispatcher, public heartbeater, public scheduler<> { @@ -87,13 +79,11 @@ namespace dripline enum class status { nothing = 0, - channel_created = 10, - exchange_declared = 20, - queue_declared = 30, - queue_bound = 40, - consuming = 50, - listening = 60, - processing = 70 + connected = 10, + topology_set = 30, + queues_bound = 40, + threads_started = 50, + listening = 60 }; public: @@ -147,14 +137,14 @@ namespace dripline bool add_async_child( endpoint_ptr_t a_endpoint_ptr ); public: - /// Sends a request message and returns a channel on which to listen for a reply. - virtual sent_msg_pkg_ptr send( request_ptr_t a_request, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + /// Sends a request message + virtual sent_msg_pkg_ptr send( request_ptr_t a_request ) const; /// Sends a reply message - virtual sent_msg_pkg_ptr send( reply_ptr_t a_reply, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + virtual sent_msg_pkg_ptr send( reply_ptr_t a_reply ) const; /// Sends an alert message - virtual sent_msg_pkg_ptr send( alert_ptr_t a_alert, amqp_channel_ptr a_channel = amqp_channel_ptr() ) const; + virtual sent_msg_pkg_ptr send( alert_ptr_t a_alert ) const; public: /** @@ -182,40 +172,30 @@ namespace dripline /// If this returns false, the service should quit with an error bool stop(); - protected: - virtual bool open_channels(); - - virtual bool setup_queues(); - - virtual bool bind_keys(); - - virtual bool start_consuming(); + mv_accessible( uuid_t, id ); - virtual bool stop_consuming(); + public: + virtual void open_channels(); - virtual bool remove_queue(); + virtual void add_queues(); - public: - /// Waits for AMQP messages arriving on the channel - /// Returns false if the return is due to an error in this function; returns true otherwise (namely because it was canceled) - virtual bool listen_on_queue(); + virtual void bind_keys(); - /// Sends a reply message - virtual void send_reply( reply_ptr_t a_reply ) const; + virtual void start_threads(); - mv_accessible( uuid_t, id ); + virtual void stop_threads(); public: typedef std::map< std::string, endpoint_ptr_t > sync_map_t; mv_referrable( sync_map_t, sync_children ); - typedef std::map< std::string, lr_ptr_t > async_map_t; + typedef std::map< std::string, elr_ptr_t > async_map_t; mv_referrable( async_map_t, async_children ); mv_referrable( std::string, broadcast_key ); protected: - /// Implementation of submit_message (from concurrent_receiver) + /// Implementation of submit_message (from message_dispatcher) virtual void submit_message( message_ptr_t a_message ); virtual reply_ptr_t on_request_message( const request_ptr_t a_request ); @@ -224,28 +204,22 @@ namespace dripline virtual void do_cancellation( int a_code ); }; - inline sent_msg_pkg_ptr service::send( request_ptr_t a_request, amqp_channel_ptr a_channel ) const + inline sent_msg_pkg_ptr service::send( request_ptr_t a_request ) const { a_request->sender_service_name() = f_name; - // we don't use f_channel on this core::send command because a channel can only be used in a single thread, - // and f_channel is primarily meant for listening with the listener thread. - return core::send( a_request, a_channel ); + return core::send( a_request ); } - inline sent_msg_pkg_ptr service::send( reply_ptr_t a_reply, amqp_channel_ptr a_channel ) const + inline sent_msg_pkg_ptr service::send( reply_ptr_t a_reply ) const { - a_reply->sender_service_name() = f_name ; - // we don't use f_channel on this core::send command because a channel can only be used in a single thread, - // and f_channel is primarily meant for listening with the listener thread. - return core::send( a_reply, a_channel ); + a_reply->sender_service_name() = f_name; + return core::send( a_reply ); } - inline sent_msg_pkg_ptr service::send( alert_ptr_t a_alert, amqp_channel_ptr a_channel ) const + inline sent_msg_pkg_ptr service::send( alert_ptr_t a_alert ) const { a_alert->sender_service_name() = f_name; - // we don't use f_channel on this core::send command because a channel can only be used in a single thread, - // and f_channel is primarily meant for listening with the listener thread. - return core::send( a_alert, a_channel ); + return core::send( a_alert ); } } /* namespace dripline */ diff --git a/testing/catch/catch.hpp b/testing/catch/catch.hpp deleted file mode 100644 index 47cd43a7..00000000 --- a/testing/catch/catch.hpp +++ /dev/null @@ -1,17975 +0,0 @@ -/* - * Catch v2.13.10 - * Generated: 2022-10-16 11:01:23.452308 - * ---------------------------------------------------------- - * This file has been merged from multiple headers. Please don't edit it directly - * Copyright (c) 2022 Two Blue Cubes Ltd. All rights reserved. - * - * Distributed under the Boost Software License, Version 1.0. (See accompanying - * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - */ -#ifndef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED -#define TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED -// start catch.hpp - - -#define CATCH_VERSION_MAJOR 2 -#define CATCH_VERSION_MINOR 13 -#define CATCH_VERSION_PATCH 10 - -#ifdef __clang__ -# pragma clang system_header -#elif defined __GNUC__ -# pragma GCC system_header -#endif - -// start catch_suppress_warnings.h - -#ifdef __clang__ -# ifdef __ICC // icpc defines the __clang__ macro -# pragma warning(push) -# pragma warning(disable: 161 1682) -# else // __ICC -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wpadded" -# pragma clang diagnostic ignored "-Wswitch-enum" -# pragma clang diagnostic ignored "-Wcovered-switch-default" -# endif -#elif defined __GNUC__ - // Because REQUIREs trigger GCC's -Wparentheses, and because still - // supported version of g++ have only buggy support for _Pragmas, - // Wparentheses have to be suppressed globally. -# pragma GCC diagnostic ignored "-Wparentheses" // See #674 for details - -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wunused-variable" -# pragma GCC diagnostic ignored "-Wpadded" -#endif -// end catch_suppress_warnings.h -#if defined(CATCH_CONFIG_MAIN) || defined(CATCH_CONFIG_RUNNER) -# define CATCH_IMPL -# define CATCH_CONFIG_ALL_PARTS -#endif - -// In the impl file, we want to have access to all parts of the headers -// Can also be used to sanely support PCHs -#if defined(CATCH_CONFIG_ALL_PARTS) -# define CATCH_CONFIG_EXTERNAL_INTERFACES -# if defined(CATCH_CONFIG_DISABLE_MATCHERS) -# undef CATCH_CONFIG_DISABLE_MATCHERS -# endif -# if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER) -# define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER -# endif -#endif - -#if !defined(CATCH_CONFIG_IMPL_ONLY) -// start catch_platform.h - -// See e.g.: -// https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/TargetConditionals.h.auto.html -#ifdef __APPLE__ -# include -# if (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) || \ - (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1) -# define CATCH_PLATFORM_MAC -# elif (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) -# define CATCH_PLATFORM_IPHONE -# endif - -#elif defined(linux) || defined(__linux) || defined(__linux__) -# define CATCH_PLATFORM_LINUX - -#elif defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) || defined(__MINGW32__) -# define CATCH_PLATFORM_WINDOWS -#endif - -// end catch_platform.h - -#ifdef CATCH_IMPL -# ifndef CLARA_CONFIG_MAIN -# define CLARA_CONFIG_MAIN_NOT_DEFINED -# define CLARA_CONFIG_MAIN -# endif -#endif - -// start catch_user_interfaces.h - -namespace Catch { - unsigned int rngSeed(); -} - -// end catch_user_interfaces.h -// start catch_tag_alias_autoregistrar.h - -// start catch_common.h - -// start catch_compiler_capabilities.h - -// Detect a number of compiler features - by compiler -// The following features are defined: -// -// CATCH_CONFIG_COUNTER : is the __COUNTER__ macro supported? -// CATCH_CONFIG_WINDOWS_SEH : is Windows SEH supported? -// CATCH_CONFIG_POSIX_SIGNALS : are POSIX signals supported? -// CATCH_CONFIG_DISABLE_EXCEPTIONS : Are exceptions enabled? -// **************** -// Note to maintainers: if new toggles are added please document them -// in configuration.md, too -// **************** - -// In general each macro has a _NO_ form -// (e.g. CATCH_CONFIG_NO_POSIX_SIGNALS) which disables the feature. -// Many features, at point of detection, define an _INTERNAL_ macro, so they -// can be combined, en-mass, with the _NO_ forms later. - -#ifdef __cplusplus - -# if (__cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L) -# define CATCH_CPP14_OR_GREATER -# endif - -# if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) -# define CATCH_CPP17_OR_GREATER -# endif - -#endif - -// Only GCC compiler should be used in this block, so other compilers trying to -// mask themselves as GCC should be ignored. -#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && !defined(__CUDACC__) && !defined(__LCC__) -# define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma( "GCC diagnostic push" ) -# define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma( "GCC diagnostic pop" ) - -# define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__) - -#endif - -#if defined(__clang__) - -# define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma( "clang diagnostic push" ) -# define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma( "clang diagnostic pop" ) - -// As of this writing, IBM XL's implementation of __builtin_constant_p has a bug -// which results in calls to destructors being emitted for each temporary, -// without a matching initialization. In practice, this can result in something -// like `std::string::~string` being called on an uninitialized value. -// -// For example, this code will likely segfault under IBM XL: -// ``` -// REQUIRE(std::string("12") + "34" == "1234") -// ``` -// -// Therefore, `CATCH_INTERNAL_IGNORE_BUT_WARN` is not implemented. -# if !defined(__ibmxl__) && !defined(__CUDACC__) -# define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__) /* NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg) */ -# endif - -# define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ - _Pragma( "clang diagnostic ignored \"-Wexit-time-destructors\"" ) \ - _Pragma( "clang diagnostic ignored \"-Wglobal-constructors\"") - -# define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS \ - _Pragma( "clang diagnostic ignored \"-Wparentheses\"" ) - -# define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS \ - _Pragma( "clang diagnostic ignored \"-Wunused-variable\"" ) - -# define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS \ - _Pragma( "clang diagnostic ignored \"-Wgnu-zero-variadic-macro-arguments\"" ) - -# define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS \ - _Pragma( "clang diagnostic ignored \"-Wunused-template\"" ) - -#endif // __clang__ - -//////////////////////////////////////////////////////////////////////////////// -// Assume that non-Windows platforms support posix signals by default -#if !defined(CATCH_PLATFORM_WINDOWS) - #define CATCH_INTERNAL_CONFIG_POSIX_SIGNALS -#endif - -//////////////////////////////////////////////////////////////////////////////// -// We know some environments not to support full POSIX signals -#if defined(__CYGWIN__) || defined(__QNX__) || defined(__EMSCRIPTEN__) || defined(__DJGPP__) - #define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS -#endif - -#ifdef __OS400__ -# define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS -# define CATCH_CONFIG_COLOUR_NONE -#endif - -//////////////////////////////////////////////////////////////////////////////// -// Android somehow still does not support std::to_string -#if defined(__ANDROID__) -# define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING -# define CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE -#endif - -//////////////////////////////////////////////////////////////////////////////// -// Not all Windows environments support SEH properly -#if defined(__MINGW32__) -# define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH -#endif - -//////////////////////////////////////////////////////////////////////////////// -// PS4 -#if defined(__ORBIS__) -# define CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE -#endif - -//////////////////////////////////////////////////////////////////////////////// -// Cygwin -#ifdef __CYGWIN__ - -// Required for some versions of Cygwin to declare gettimeofday -// see: http://stackoverflow.com/questions/36901803/gettimeofday-not-declared-in-this-scope-cygwin -# define _BSD_SOURCE -// some versions of cygwin (most) do not support std::to_string. Use the libstd check. -// https://gcc.gnu.org/onlinedocs/gcc-4.8.2/libstdc++/api/a01053_source.html line 2812-2813 -# if !((__cplusplus >= 201103L) && defined(_GLIBCXX_USE_C99) \ - && !defined(_GLIBCXX_HAVE_BROKEN_VSWPRINTF)) - -# define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING - -# endif -#endif // __CYGWIN__ - -//////////////////////////////////////////////////////////////////////////////// -// Visual C++ -#if defined(_MSC_VER) - -// Universal Windows platform does not support SEH -// Or console colours (or console at all...) -# if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) -# define CATCH_CONFIG_COLOUR_NONE -# else -# define CATCH_INTERNAL_CONFIG_WINDOWS_SEH -# endif - -# if !defined(__clang__) // Handle Clang masquerading for msvc - -// MSVC traditional preprocessor needs some workaround for __VA_ARGS__ -// _MSVC_TRADITIONAL == 0 means new conformant preprocessor -// _MSVC_TRADITIONAL == 1 means old traditional non-conformant preprocessor -# if !defined(_MSVC_TRADITIONAL) || (defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL) -# define CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -# endif // MSVC_TRADITIONAL - -// Only do this if we're not using clang on Windows, which uses `diagnostic push` & `diagnostic pop` -# define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION __pragma( warning(push) ) -# define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION __pragma( warning(pop) ) -# endif // __clang__ - -#endif // _MSC_VER - -#if defined(_REENTRANT) || defined(_MSC_VER) -// Enable async processing, as -pthread is specified or no additional linking is required -# define CATCH_INTERNAL_CONFIG_USE_ASYNC -#endif // _MSC_VER - -//////////////////////////////////////////////////////////////////////////////// -// Check if we are compiled with -fno-exceptions or equivalent -#if defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND) -# define CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED -#endif - -//////////////////////////////////////////////////////////////////////////////// -// DJGPP -#ifdef __DJGPP__ -# define CATCH_INTERNAL_CONFIG_NO_WCHAR -#endif // __DJGPP__ - -//////////////////////////////////////////////////////////////////////////////// -// Embarcadero C++Build -#if defined(__BORLANDC__) - #define CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN -#endif - -//////////////////////////////////////////////////////////////////////////////// - -// Use of __COUNTER__ is suppressed during code analysis in -// CLion/AppCode 2017.2.x and former, because __COUNTER__ is not properly -// handled by it. -// Otherwise all supported compilers support COUNTER macro, -// but user still might want to turn it off -#if ( !defined(__JETBRAINS_IDE__) || __JETBRAINS_IDE__ >= 20170300L ) - #define CATCH_INTERNAL_CONFIG_COUNTER -#endif - -//////////////////////////////////////////////////////////////////////////////// - -// RTX is a special version of Windows that is real time. -// This means that it is detected as Windows, but does not provide -// the same set of capabilities as real Windows does. -#if defined(UNDER_RTSS) || defined(RTX64_BUILD) - #define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH - #define CATCH_INTERNAL_CONFIG_NO_ASYNC - #define CATCH_CONFIG_COLOUR_NONE -#endif - -#if !defined(_GLIBCXX_USE_C99_MATH_TR1) -#define CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER -#endif - -// Various stdlib support checks that require __has_include -#if defined(__has_include) - // Check if string_view is available and usable - #if __has_include() && defined(CATCH_CPP17_OR_GREATER) - # define CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW - #endif - - // Check if optional is available and usable - # if __has_include() && defined(CATCH_CPP17_OR_GREATER) - # define CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL - # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) - - // Check if byte is available and usable - # if __has_include() && defined(CATCH_CPP17_OR_GREATER) - # include - # if defined(__cpp_lib_byte) && (__cpp_lib_byte > 0) - # define CATCH_INTERNAL_CONFIG_CPP17_BYTE - # endif - # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) - - // Check if variant is available and usable - # if __has_include() && defined(CATCH_CPP17_OR_GREATER) - # if defined(__clang__) && (__clang_major__ < 8) - // work around clang bug with libstdc++ https://bugs.llvm.org/show_bug.cgi?id=31852 - // fix should be in clang 8, workaround in libstdc++ 8.2 - # include - # if defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) - # define CATCH_CONFIG_NO_CPP17_VARIANT - # else - # define CATCH_INTERNAL_CONFIG_CPP17_VARIANT - # endif // defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) - # else - # define CATCH_INTERNAL_CONFIG_CPP17_VARIANT - # endif // defined(__clang__) && (__clang_major__ < 8) - # endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) -#endif // defined(__has_include) - -#if defined(CATCH_INTERNAL_CONFIG_COUNTER) && !defined(CATCH_CONFIG_NO_COUNTER) && !defined(CATCH_CONFIG_COUNTER) -# define CATCH_CONFIG_COUNTER -#endif -#if defined(CATCH_INTERNAL_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_NO_WINDOWS_SEH) && !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH) -# define CATCH_CONFIG_WINDOWS_SEH -#endif -// This is set by default, because we assume that unix compilers are posix-signal-compatible by default. -#if defined(CATCH_INTERNAL_CONFIG_POSIX_SIGNALS) && !defined(CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_POSIX_SIGNALS) -# define CATCH_CONFIG_POSIX_SIGNALS -#endif -// This is set by default, because we assume that compilers with no wchar_t support are just rare exceptions. -#if !defined(CATCH_INTERNAL_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_WCHAR) -# define CATCH_CONFIG_WCHAR -#endif - -#if !defined(CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_CPP11_TO_STRING) -# define CATCH_CONFIG_CPP11_TO_STRING -#endif - -#if defined(CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_NO_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_CPP17_OPTIONAL) -# define CATCH_CONFIG_CPP17_OPTIONAL -#endif - -#if defined(CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_NO_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_CPP17_STRING_VIEW) -# define CATCH_CONFIG_CPP17_STRING_VIEW -#endif - -#if defined(CATCH_INTERNAL_CONFIG_CPP17_VARIANT) && !defined(CATCH_CONFIG_NO_CPP17_VARIANT) && !defined(CATCH_CONFIG_CPP17_VARIANT) -# define CATCH_CONFIG_CPP17_VARIANT -#endif - -#if defined(CATCH_INTERNAL_CONFIG_CPP17_BYTE) && !defined(CATCH_CONFIG_NO_CPP17_BYTE) && !defined(CATCH_CONFIG_CPP17_BYTE) -# define CATCH_CONFIG_CPP17_BYTE -#endif - -#if defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT) -# define CATCH_INTERNAL_CONFIG_NEW_CAPTURE -#endif - -#if defined(CATCH_INTERNAL_CONFIG_NEW_CAPTURE) && !defined(CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NEW_CAPTURE) -# define CATCH_CONFIG_NEW_CAPTURE -#endif - -#if !defined(CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) -# define CATCH_CONFIG_DISABLE_EXCEPTIONS -#endif - -#if defined(CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_NO_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_POLYFILL_ISNAN) -# define CATCH_CONFIG_POLYFILL_ISNAN -#endif - -#if defined(CATCH_INTERNAL_CONFIG_USE_ASYNC) && !defined(CATCH_INTERNAL_CONFIG_NO_ASYNC) && !defined(CATCH_CONFIG_NO_USE_ASYNC) && !defined(CATCH_CONFIG_USE_ASYNC) -# define CATCH_CONFIG_USE_ASYNC -#endif - -#if defined(CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_NO_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_ANDROID_LOGWRITE) -# define CATCH_CONFIG_ANDROID_LOGWRITE -#endif - -#if defined(CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_NO_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_GLOBAL_NEXTAFTER) -# define CATCH_CONFIG_GLOBAL_NEXTAFTER -#endif - -// Even if we do not think the compiler has that warning, we still have -// to provide a macro that can be used by the code. -#if !defined(CATCH_INTERNAL_START_WARNINGS_SUPPRESSION) -# define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION -#endif -#if !defined(CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION) -# define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION -#endif -#if !defined(CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS) -# define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS -#endif -#if !defined(CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS) -# define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS -#endif -#if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS) -# define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS -#endif -#if !defined(CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS) -# define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS -#endif - -// The goal of this macro is to avoid evaluation of the arguments, but -// still have the compiler warn on problems inside... -#if !defined(CATCH_INTERNAL_IGNORE_BUT_WARN) -# define CATCH_INTERNAL_IGNORE_BUT_WARN(...) -#endif - -#if defined(__APPLE__) && defined(__apple_build_version__) && (__clang_major__ < 10) -# undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS -#elif defined(__clang__) && (__clang_major__ < 5) -# undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS -#endif - -#if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS) -# define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS -#endif - -#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) -#define CATCH_TRY if ((true)) -#define CATCH_CATCH_ALL if ((false)) -#define CATCH_CATCH_ANON(type) if ((false)) -#else -#define CATCH_TRY try -#define CATCH_CATCH_ALL catch (...) -#define CATCH_CATCH_ANON(type) catch (type) -#endif - -#if defined(CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_NO_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) -#define CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#endif - -// end catch_compiler_capabilities.h -#define INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) name##line -#define INTERNAL_CATCH_UNIQUE_NAME_LINE( name, line ) INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) -#ifdef CATCH_CONFIG_COUNTER -# define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __COUNTER__ ) -#else -# define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __LINE__ ) -#endif - -#include -#include -#include - -// We need a dummy global operator<< so we can bring it into Catch namespace later -struct Catch_global_namespace_dummy {}; -std::ostream& operator<<(std::ostream&, Catch_global_namespace_dummy); - -namespace Catch { - - struct CaseSensitive { enum Choice { - Yes, - No - }; }; - - class NonCopyable { - NonCopyable( NonCopyable const& ) = delete; - NonCopyable( NonCopyable && ) = delete; - NonCopyable& operator = ( NonCopyable const& ) = delete; - NonCopyable& operator = ( NonCopyable && ) = delete; - - protected: - NonCopyable(); - virtual ~NonCopyable(); - }; - - struct SourceLineInfo { - - SourceLineInfo() = delete; - SourceLineInfo( char const* _file, std::size_t _line ) noexcept - : file( _file ), - line( _line ) - {} - - SourceLineInfo( SourceLineInfo const& other ) = default; - SourceLineInfo& operator = ( SourceLineInfo const& ) = default; - SourceLineInfo( SourceLineInfo&& ) noexcept = default; - SourceLineInfo& operator = ( SourceLineInfo&& ) noexcept = default; - - bool empty() const noexcept { return file[0] == '\0'; } - bool operator == ( SourceLineInfo const& other ) const noexcept; - bool operator < ( SourceLineInfo const& other ) const noexcept; - - char const* file; - std::size_t line; - }; - - std::ostream& operator << ( std::ostream& os, SourceLineInfo const& info ); - - // Bring in operator<< from global namespace into Catch namespace - // This is necessary because the overload of operator<< above makes - // lookup stop at namespace Catch - using ::operator<<; - - // Use this in variadic streaming macros to allow - // >> +StreamEndStop - // as well as - // >> stuff +StreamEndStop - struct StreamEndStop { - std::string operator+() const; - }; - template - T const& operator + ( T const& value, StreamEndStop ) { - return value; - } -} - -#define CATCH_INTERNAL_LINEINFO \ - ::Catch::SourceLineInfo( __FILE__, static_cast( __LINE__ ) ) - -// end catch_common.h -namespace Catch { - - struct RegistrarForTagAliases { - RegistrarForTagAliases( char const* alias, char const* tag, SourceLineInfo const& lineInfo ); - }; - -} // end namespace Catch - -#define CATCH_REGISTER_TAG_ALIAS( alias, spec ) \ - CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ - CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ - namespace{ Catch::RegistrarForTagAliases INTERNAL_CATCH_UNIQUE_NAME( AutoRegisterTagAlias )( alias, spec, CATCH_INTERNAL_LINEINFO ); } \ - CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION - -// end catch_tag_alias_autoregistrar.h -// start catch_test_registry.h - -// start catch_interfaces_testcase.h - -#include - -namespace Catch { - - class TestSpec; - - struct ITestInvoker { - virtual void invoke () const = 0; - virtual ~ITestInvoker(); - }; - - class TestCase; - struct IConfig; - - struct ITestCaseRegistry { - virtual ~ITestCaseRegistry(); - virtual std::vector const& getAllTests() const = 0; - virtual std::vector const& getAllTestsSorted( IConfig const& config ) const = 0; - }; - - bool isThrowSafe( TestCase const& testCase, IConfig const& config ); - bool matchTest( TestCase const& testCase, TestSpec const& testSpec, IConfig const& config ); - std::vector filterTests( std::vector const& testCases, TestSpec const& testSpec, IConfig const& config ); - std::vector const& getAllTestCasesSorted( IConfig const& config ); - -} - -// end catch_interfaces_testcase.h -// start catch_stringref.h - -#include -#include -#include -#include - -namespace Catch { - - /// A non-owning string class (similar to the forthcoming std::string_view) - /// Note that, because a StringRef may be a substring of another string, - /// it may not be null terminated. - class StringRef { - public: - using size_type = std::size_t; - using const_iterator = const char*; - - private: - static constexpr char const* const s_empty = ""; - - char const* m_start = s_empty; - size_type m_size = 0; - - public: // construction - constexpr StringRef() noexcept = default; - - StringRef( char const* rawChars ) noexcept; - - constexpr StringRef( char const* rawChars, size_type size ) noexcept - : m_start( rawChars ), - m_size( size ) - {} - - StringRef( std::string const& stdString ) noexcept - : m_start( stdString.c_str() ), - m_size( stdString.size() ) - {} - - explicit operator std::string() const { - return std::string(m_start, m_size); - } - - public: // operators - auto operator == ( StringRef const& other ) const noexcept -> bool; - auto operator != (StringRef const& other) const noexcept -> bool { - return !(*this == other); - } - - auto operator[] ( size_type index ) const noexcept -> char { - assert(index < m_size); - return m_start[index]; - } - - public: // named queries - constexpr auto empty() const noexcept -> bool { - return m_size == 0; - } - constexpr auto size() const noexcept -> size_type { - return m_size; - } - - // Returns the current start pointer. If the StringRef is not - // null-terminated, throws std::domain_exception - auto c_str() const -> char const*; - - public: // substrings and searches - // Returns a substring of [start, start + length). - // If start + length > size(), then the substring is [start, size()). - // If start > size(), then the substring is empty. - auto substr( size_type start, size_type length ) const noexcept -> StringRef; - - // Returns the current start pointer. May not be null-terminated. - auto data() const noexcept -> char const*; - - constexpr auto isNullTerminated() const noexcept -> bool { - return m_start[m_size] == '\0'; - } - - public: // iterators - constexpr const_iterator begin() const { return m_start; } - constexpr const_iterator end() const { return m_start + m_size; } - }; - - auto operator += ( std::string& lhs, StringRef const& sr ) -> std::string&; - auto operator << ( std::ostream& os, StringRef const& sr ) -> std::ostream&; - - constexpr auto operator "" _sr( char const* rawChars, std::size_t size ) noexcept -> StringRef { - return StringRef( rawChars, size ); - } -} // namespace Catch - -constexpr auto operator "" _catch_sr( char const* rawChars, std::size_t size ) noexcept -> Catch::StringRef { - return Catch::StringRef( rawChars, size ); -} - -// end catch_stringref.h -// start catch_preprocessor.hpp - - -#define CATCH_RECURSION_LEVEL0(...) __VA_ARGS__ -#define CATCH_RECURSION_LEVEL1(...) CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(__VA_ARGS__))) -#define CATCH_RECURSION_LEVEL2(...) CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(__VA_ARGS__))) -#define CATCH_RECURSION_LEVEL3(...) CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(__VA_ARGS__))) -#define CATCH_RECURSION_LEVEL4(...) CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(__VA_ARGS__))) -#define CATCH_RECURSION_LEVEL5(...) CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(__VA_ARGS__))) - -#ifdef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define INTERNAL_CATCH_EXPAND_VARGS(...) __VA_ARGS__ -// MSVC needs more evaluations -#define CATCH_RECURSION_LEVEL6(...) CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(__VA_ARGS__))) -#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL6(CATCH_RECURSION_LEVEL6(__VA_ARGS__)) -#else -#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL5(__VA_ARGS__) -#endif - -#define CATCH_REC_END(...) -#define CATCH_REC_OUT - -#define CATCH_EMPTY() -#define CATCH_DEFER(id) id CATCH_EMPTY() - -#define CATCH_REC_GET_END2() 0, CATCH_REC_END -#define CATCH_REC_GET_END1(...) CATCH_REC_GET_END2 -#define CATCH_REC_GET_END(...) CATCH_REC_GET_END1 -#define CATCH_REC_NEXT0(test, next, ...) next CATCH_REC_OUT -#define CATCH_REC_NEXT1(test, next) CATCH_DEFER ( CATCH_REC_NEXT0 ) ( test, next, 0) -#define CATCH_REC_NEXT(test, next) CATCH_REC_NEXT1(CATCH_REC_GET_END test, next) - -#define CATCH_REC_LIST0(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) -#define CATCH_REC_LIST1(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0) ) ( f, peek, __VA_ARGS__ ) -#define CATCH_REC_LIST2(f, x, peek, ...) f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) - -#define CATCH_REC_LIST0_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) -#define CATCH_REC_LIST1_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0_UD) ) ( f, userdata, peek, __VA_ARGS__ ) -#define CATCH_REC_LIST2_UD(f, userdata, x, peek, ...) f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) - -// Applies the function macro `f` to each of the remaining parameters, inserts commas between the results, -// and passes userdata as the first parameter to each invocation, -// e.g. CATCH_REC_LIST_UD(f, x, a, b, c) evaluates to f(x, a), f(x, b), f(x, c) -#define CATCH_REC_LIST_UD(f, userdata, ...) CATCH_RECURSE(CATCH_REC_LIST2_UD(f, userdata, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) - -#define CATCH_REC_LIST(f, ...) CATCH_RECURSE(CATCH_REC_LIST2(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) - -#define INTERNAL_CATCH_EXPAND1(param) INTERNAL_CATCH_EXPAND2(param) -#define INTERNAL_CATCH_EXPAND2(...) INTERNAL_CATCH_NO## __VA_ARGS__ -#define INTERNAL_CATCH_DEF(...) INTERNAL_CATCH_DEF __VA_ARGS__ -#define INTERNAL_CATCH_NOINTERNAL_CATCH_DEF -#define INTERNAL_CATCH_STRINGIZE(...) INTERNAL_CATCH_STRINGIZE2(__VA_ARGS__) -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define INTERNAL_CATCH_STRINGIZE2(...) #__VA_ARGS__ -#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) -#else -// MSVC is adding extra space and needs another indirection to expand INTERNAL_CATCH_NOINTERNAL_CATCH_DEF -#define INTERNAL_CATCH_STRINGIZE2(...) INTERNAL_CATCH_STRINGIZE3(__VA_ARGS__) -#define INTERNAL_CATCH_STRINGIZE3(...) #__VA_ARGS__ -#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) (INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) + 1) -#endif - -#define INTERNAL_CATCH_MAKE_NAMESPACE2(...) ns_##__VA_ARGS__ -#define INTERNAL_CATCH_MAKE_NAMESPACE(name) INTERNAL_CATCH_MAKE_NAMESPACE2(name) - -#define INTERNAL_CATCH_REMOVE_PARENS(...) INTERNAL_CATCH_EXPAND1(INTERNAL_CATCH_DEF __VA_ARGS__) - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) decltype(get_wrapper()) -#define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__)) -#else -#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) INTERNAL_CATCH_EXPAND_VARGS(decltype(get_wrapper())) -#define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__))) -#endif - -#define INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(...)\ - CATCH_REC_LIST(INTERNAL_CATCH_MAKE_TYPE_LIST,__VA_ARGS__) - -#define INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_0) INTERNAL_CATCH_REMOVE_PARENS(_0) -#define INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_0, _1) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_1) -#define INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_0, _1, _2) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_1, _2) -#define INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_0, _1, _2, _3) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_1, _2, _3) -#define INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_0, _1, _2, _3, _4) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_1, _2, _3, _4) -#define INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_0, _1, _2, _3, _4, _5) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_1, _2, _3, _4, _5) -#define INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_0, _1, _2, _3, _4, _5, _6) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_1, _2, _3, _4, _5, _6) -#define INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_0, _1, _2, _3, _4, _5, _6, _7) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_1, _2, _3, _4, _5, _6, _7) -#define INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_1, _2, _3, _4, _5, _6, _7, _8) -#define INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9) -#define INTERNAL_CATCH_REMOVE_PARENS_11_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10) - -#define INTERNAL_CATCH_VA_NARGS_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N - -#define INTERNAL_CATCH_TYPE_GEN\ - template struct TypeList {};\ - template\ - constexpr auto get_wrapper() noexcept -> TypeList { return {}; }\ - template class...> struct TemplateTypeList{};\ - template class...Cs>\ - constexpr auto get_wrapper() noexcept -> TemplateTypeList { return {}; }\ - template\ - struct append;\ - template\ - struct rewrap;\ - template class, typename...>\ - struct create;\ - template class, typename>\ - struct convert;\ - \ - template \ - struct append { using type = T; };\ - template< template class L1, typename...E1, template class L2, typename...E2, typename...Rest>\ - struct append, L2, Rest...> { using type = typename append, Rest...>::type; };\ - template< template class L1, typename...E1, typename...Rest>\ - struct append, TypeList, Rest...> { using type = L1; };\ - \ - template< template class Container, template class List, typename...elems>\ - struct rewrap, List> { using type = TypeList>; };\ - template< template class Container, template class List, class...Elems, typename...Elements>\ - struct rewrap, List, Elements...> { using type = typename append>, typename rewrap, Elements...>::type>::type; };\ - \ - template