diff --git a/src/.clang-format b/.clang-format similarity index 99% rename from src/.clang-format rename to .clang-format index 2f771200..fdfa11f5 100644 --- a/src/.clang-format +++ b/.clang-format @@ -18,7 +18,6 @@ AlignOperands: false AlignTrailingComments: true AllowShortBlocksOnASingleLine: true AllowShortIfStatementsOnASingleLine: true -AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: false # AllowShortFunctionsOnASingleLine: InlineOnly # AllowShortLoopsOnASingleLine: false diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..d39809e7 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,89 @@ +codecov: + notify: + require_ci_to_pass: no + +ignore: + - "**/*_test.c*" + - "**/*_test.h*" + - "**/*_generated.h" + - "**/*_pb.cc" + - "**/*_pb.h" + - "**/test*/*.c*" + - "**/test*/*.h*" + - "**/*test_flip.*" + - "**/grpc/tests/**" + +comment: + layout: "header, diff, flags, components" + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + - component_id: modules_auth_manager + name: AuthManager + paths: + - src/auth_manager/** + - include/sisl/auth_manager/** + - component_id: modules_cache + name: Cache + paths: + - src/cache/** + - include/sisl/cache/** + - component_id: modules_fds + name: FDS + paths: + - src/fds/** + - include/sisl/fds/** + - component_id: modules_file_watcher + name: FileWatcher + paths: + - src/file_watcher/** + - include/sisl/file_watcher/** + - component_id: modules_flip + name: Flip + paths: + - src/flip/** + - include/sisl/flip/** + - component_id: modules_grpc + name: gRPC + paths: + - src/grpc/** + - include/sisl/grpc/** + - component_id: modules_logging + name: Logging + paths: + - src/logging/** + - include/sisl/logging/** + - component_id: modules_metrics + name: Metrics + paths: + - src/metrics/** + - include/sisl/metrics/** + - component_id: modules_options + name: Options + paths: + - src/options/** + - include/sisl/options/** + - component_id: modules_settings + name: Setting + paths: + - src/settings/** + - include/sisl/settings/** + - component_id: modules_sobject + name: StatusObject + paths: + - src/sobject/** + - include/sisl/sobject/** + - component_id: modules_version + name: Utility + paths: + - src/utility/** + - include/sisl/utility/** + - component_id: modules_version + name: Version + paths: + - src/version/** + - include/sisl/version.hpp diff --git a/.github/actions/load_conan/action.yml b/.github/actions/load_conan/action.yml new file mode 100644 index 00000000..22991f83 --- /dev/null +++ b/.github/actions/load_conan/action.yml @@ -0,0 +1,57 @@ +name: 'Load Conan Cache' +description: 'Loads Local Conan Cache' +inputs: + testing: + description: 'Support building tests' + required: true + key_prefix: + description: 'Cache prefix' + required: true + default: 'Deps' + fail_on_cache_miss: + description: 'Fail if key missing' + required: false + default: false + path: + description: 'Recipe path' + required: false + default: '.' + load_any: + description: 'Load cache miss' + required: false + default: 'False' +outputs: + cache-hit: + description: 'Cache match found' + value: ${{ steps.restore-cache.outputs.cache-hit }} +runs: + using: "composite" + steps: + - id: hash-key-primary + shell: bash + run: | + echo "key=${{ inputs.path }}/conanfile.py" >> $GITHUB_OUTPUT + + - id: hash-key-3rd + shell: bash + run: | + echo "keys=${{ inputs.path }}/3rd_party/**/conanfile.py" >> $GITHUB_OUTPUT + + - name: Restore Cache + id: restore-cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.conan/data + key: ${{ inputs.key_prefix }}-${{ hashFiles(steps.hash-key-primary.outputs.key, steps.hash-key-3rd.outputs.keys) }} + fail-on-cache-miss: ${{ inputs.fail_on_cache_miss }} + + - name: Restore Testing Cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.conan/data + key: ${{ inputs.key_prefix }}-${{ hashFiles(steps.hash-key-primary.outputs.key, steps.hash-key-3rd.outputs.keys) }} + restore-keys: ${{ inputs.key_prefix }}- + if: ${{ steps.restore-cache.outputs.cache-hit != 'true' && (( github.event_name == 'pull_request' && inputs.testing == 'True' ) || ( inputs.load_any == 'True' )) }} + diff --git a/.github/actions/setup_conan/action.yml b/.github/actions/setup_conan/action.yml new file mode 100644 index 00000000..0d3dfe01 --- /dev/null +++ b/.github/actions/setup_conan/action.yml @@ -0,0 +1,31 @@ +name: 'Setup Conan' +description: 'Sets up Conan for Sisl Builds' +inputs: + platform: + description: 'Platform conan will be building on' + required: true + default: 'ubuntu-22.04' +runs: + using: "composite" + steps: + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: "3.8" + + - name: Setup Conan and Export Recipes + shell: bash + run: | + python -m pip install --upgrade pip + python -m pip install conan~=1.0 + python -m pip install gcovr + conan user + conan profile new --detect default + + - name: Fixup libstdc++ + shell: bash + run: | + # Set std::string to non-CoW C++11 version + sed -i 's,compiler.libcxx=libstdc++$,compiler.libcxx=libstdc++11,g' ~/.conan/profiles/default + if: ${{ inputs.platform == 'ubuntu-22.04' }} + diff --git a/.github/actions/store_conan/action.yml b/.github/actions/store_conan/action.yml new file mode 100644 index 00000000..065614fa --- /dev/null +++ b/.github/actions/store_conan/action.yml @@ -0,0 +1,36 @@ +name: 'Store Conan Cache' +description: 'Cleans Local Conan Cache and Persists Dirty Packages' +inputs: + key_prefix: + description: 'Cache prefix' + required: true + default: 'Deps' +runs: + using: "composite" + steps: + - name: Setup Conan and Export Recipes + shell: bash + run: | + if [ -d 3rd_party ]; then + dep_pkgs=$(ls -1d 3rd_party/* 2>/dev/null | cut -d'/' -f2 | paste -sd'|' - -) + fi + if [ -z "${dep_pkgs}" ]; then + dep_pkgs="no_3rd_party" + fi + dirty_pkgs=$(ls -1d ~/.conan/data/*/*/*/*/build 2>/dev/null | sed 's,.*data/,,') + if [ -z "${dirty_pkgs}" ]; then + dirty_pkgs="no_public/0" + fi + dirty_pkgs_d=$(echo "${dirty_pkgs}" | cut -d'/' -f1 | paste -sd'|' - -) + echo "::info:: Caching: ${dirty_pkgs_d}|${dep_pkgs}" + ls -1d ~/.conan/data/* | grep -Ev "(${dirty_pkgs_d}|${dep_pkgs})" | xargs rm -rf + rm -rf ~/.conan/data/*/*/*/*/build + rm -rf ~/.conan/data/*/*/*/*/source + + - name: Save Cache + uses: actions/cache/save@v3 + with: + path: | + ~/.conan/data + key: ${{ inputs.key_prefix }}-${{ hashFiles('conanfile.py', '3rd_party/**/conanfile.py') }} + diff --git a/.github/workflows/build_dependencies.yml b/.github/workflows/build_dependencies.yml new file mode 100644 index 00000000..a365256d --- /dev/null +++ b/.github/workflows/build_dependencies.yml @@ -0,0 +1,166 @@ +name: Conan Build + +on: + workflow_call: + inputs: + platform: + required: false + default: 'ubuntu-22.04' + type: string + branch: + required: true + type: string + build-type: + required: true + type: string + malloc-impl: + required: true + type: string + prerelease: + required: false + type: string + default: 'False' + tooling: + required: false + type: string + default: 'None' + testing: + required: false + type: string + default: 'False' + workflow_dispatch: + inputs: + platform: + required: true + type: choice + options: + - ubuntu-22.04 + - ubuntu-20.04 + - macos-13 + - macos-12 + default: 'ubuntu-22.04' + branch: + required: true + type: string + build-type: + required: true + type: choice + options: + - Debug + - Release + - RelWithDebInfo + malloc-impl: + description: 'Allocation Library' + required: true + type: choice + options: + - libc + - tcmalloc + - jemalloc + prerelease: + description: 'Fault Instrumentation' + required: false + type: choice + options: + - 'True' + - 'False' + default: 'False' + tooling: + required: false + type: choice + - 'Sanitize' + - 'Coverage' + - 'None' + default: 'None' + testing: + description: 'Build and Run' + required: true + type: choice + options: + - 'True' + - 'False' + default: 'True' + +jobs: + BuildSislDeps: + runs-on: ${{ inputs.platform }} + steps: + - name: Retrieve Code + uses: actions/checkout@v3 + with: + ref: ${{ inputs.branch }} + if: ${{ inputs.testing == 'True' }} + + - name: Retrieve Recipe + uses: actions/checkout@v3 + with: + repository: eBay/sisl + ref: ${{ inputs.branch }} + if: ${{ inputs.testing == 'False' }} + + - name: Load Conan Cache + id: restore-cache + uses: eBay/sisl/.github/actions/load_conan@stable/v8.x + with: + testing: ${{ inputs.testing }} + key_prefix: SislDeps-${{ inputs.platform }}-${{ inputs.build-type }}-${{ inputs.malloc-impl }}-${{ inputs.prerelease }} + + - name: Setup Conan + uses: eBay/sisl/.github/actions/setup_conan@stable/v8.x + with: + platform: ${{ inputs.platform }} + if: ${{ inputs.testing == 'True' || steps.restore-cache.outputs.cache-hit != 'true' }} + + - name: Prepare Recipes + run: | + ./prepare.sh + cached_pkgs=$(ls -1d ~/.conan/data/*/*/*/*/export 2>/dev/null | sed 's,.*data/,,' | cut -d'/' -f1,2 | paste -sd',' - -) + echo "::info:: Pre-cached: ${cached_pkgs}" + if: ${{ inputs.testing == 'True' || steps.restore-cache.outputs.cache-hit != 'true' }} + + - name: Build Cache + run: | + conan install \ + -o prerelease=${{ inputs.prerelease }} \ + -o malloc_impl=${{ inputs.malloc-impl }} \ + -s build_type=${{ inputs.build-type }} \ + --build missing \ + . + if: ${{ steps.restore-cache.outputs.cache-hit != 'true' }} + + - name: Save Conan Cache + uses: eBay/sisl/.github/actions/store_conan@stable/v8.x + with: + key_prefix: SislDeps-${{ inputs.platform }}-${{ inputs.build-type }}-${{ inputs.malloc-impl }}-${{ inputs.prerelease }} + if: ${{ github.event_name != 'pull_request' && steps.restore-cache.outputs.cache-hit != 'true' }} + + - name: Create and Test Package + run: | + sanitize=$([[ "${{ inputs.tooling }}" == "Sanitize" ]] && echo "True" || echo "False") + conan create \ + -o sisl:prerelease=${{ inputs.prerelease }} \ + -o sisl:malloc_impl=${{ inputs.malloc-impl }} \ + -o sisl:sanitize=${sanitize} \ + -s build_type=${{ inputs.build-type }} \ + --build missing \ + . + if: ${{ inputs.testing == 'True' && inputs.tooling != 'Coverage' }} + + - name: Code Coverage Run + run: | + conan install \ + -o prerelease=${{ inputs.prerelease }} \ + -o malloc_impl=${{ inputs.malloc-impl }} \ + -o coverage=True \ + -s build_type=${{ inputs.build-type }} \ + --build missing \ + . + conan build . + if: ${{ inputs.testing == 'True' && inputs.tooling == 'Coverage' }} + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + gcov: true + if: ${{ inputs.testing == 'True' && inputs.tooling == 'Coverage' }} diff --git a/.github/workflows/build_with_conan.yml b/.github/workflows/build_with_conan.yml deleted file mode 100644 index d3d5773b..00000000 --- a/.github/workflows/build_with_conan.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Conan Build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -#env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - #BUILD_TYPE: Release - -jobs: - build: - # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. - # You can convert this to a matrix build if you need cross-platform coverage. - # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - python-version: ["3.8"] - build-type: ["Debug", "Release"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install Conan - run: | - python -m pip install --upgrade pip - python -m pip install conan - - - name: Configure Conan - # Configure conan profiles for build runner - run: | - conan user - - - name: Install dependencies - # Build your program with the given configuration - run: | - conan install -s build_type=${{ matrix.build-type }} --build missing . - - - name: Build - # Build your program with the given configuration - run: | - conan build . diff --git a/.github/workflows/merge_build.yml b/.github/workflows/merge_build.yml new file mode 100644 index 00000000..4aa16cff --- /dev/null +++ b/.github/workflows/merge_build.yml @@ -0,0 +1,64 @@ +name: Sisl Build + +on: + workflow_dispatch: + push: + branches: + - stable/v8.x + - master + +jobs: + Build: + strategy: + fail-fast: false + matrix: + platform: ["ubuntu-22.04"] + build-type: ["Debug", "Release"] + malloc-impl: ["libc", "tcmalloc"] + prerelease: ["True", "False"] + tooling: ["Sanitize", "Coverage", "None"] + exclude: + - build-type: Debug + prerelease: "False" + - build-type: Debug + tooling: None + - build-type: Debug + malloc-impl: tcmalloc + - build-type: Release + malloc-impl: libc + - build-type: Release + tooling: Sanitize + - build-type: Release + tooling: Coverage + uses: ./.github/workflows/build_dependencies.yml + with: + platform: ${{ matrix.platform }} + branch: ${{ github.ref }} + build-type: ${{ matrix.build-type }} + malloc-impl: ${{ matrix.malloc-impl }} + prerelease: ${{ matrix.prerelease }} + tooling: ${{ matrix.tooling }} + testing: 'True' + ChainBuild: + runs-on: "ubuntu-22.04" + steps: + - name: Start IOManager Build + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.CHAIN_BUILD_TOKEN }}"\ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/eBay/iomanager/actions/workflows/merge_build.yml/dispatches \ + -d '{"ref":"master","inputs":{}}' + if: ${{ github.ref == 'refs/heads/master' }} + - name: Start NuraftMesg Build + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.CHAIN_BUILD_TOKEN }}"\ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/eBay/nuraft_mesg/actions/workflows/conan_build.yml/dispatches \ + -d '{"ref":"main","inputs":{}}' + if: ${{ github.ref == 'refs/heads/master' }} diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml new file mode 100644 index 00000000..41417546 --- /dev/null +++ b/.github/workflows/pr_build.yml @@ -0,0 +1,41 @@ +name: Sisl Build + +on: + workflow_dispatch: + pull_request: + branches: + - stable/v8.x + - master + +jobs: + Build: + strategy: + fail-fast: false + matrix: + platform: ["ubuntu-22.04"] + build-type: ["Debug", "Release"] + malloc-impl: ["libc", "tcmalloc"] + prerelease: ["True", "False"] + tooling: ["Sanitize", "Coverage", "None"] + exclude: + - build-type: Debug + prerelease: "False" + - build-type: Debug + tooling: None + - build-type: Debug + malloc-impl: tcmalloc + - build-type: Release + malloc-impl: libc + - build-type: Release + tooling: Sanitize + - build-type: Release + tooling: Coverage + uses: ./.github/workflows/build_dependencies.yml + with: + platform: ${{ matrix.platform }} + branch: ${{ github.ref }} + build-type: ${{ matrix.build-type }} + malloc-impl: ${{ matrix.malloc-impl }} + prerelease: ${{ matrix.prerelease }} + tooling: ${{ matrix.tooling }} + testing: 'True' diff --git a/.gitignore b/.gitignore index bd9e5aef..84d06d73 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ src/.tags* # build generated products build/** +test_package/build debug/** release/** cmake-*/** @@ -106,3 +107,8 @@ CMakeSettings.json # Clangd .cache/clangd + +CMakeUserPresets.json +logs/ +conan.lock +graph_info.json diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile index f795db11..51d3d78b 100644 --- a/.jenkins/Jenkinsfile +++ b/.jenkins/Jenkinsfile @@ -1,35 +1,28 @@ pipeline { - agent any + agent { label 'sds-builder-2204' } environment { ARTIFACTORY_PASS = credentials('ARTIFACTORY_PASS') - CONAN_USER = 'sisl' + CONAN_USER = 'oss' TARGET_BRANCH = 'master' - TESTING_BRANCH = 'testing/v*' STABLE_BRANCH = 'stable/v*' } stages { stage('Adjust Tag for Master/PR') { - when { not { anyOf { - branch "${TESTING_BRANCH}" + when { not { branch "${STABLE_BRANCH}" - } } } + } } steps { script { - sh(script: "sed -Ei 's,version = .*\"([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*,version = \"\\1-${env.BUILD_NUMBER}\",' conanfile.py") - BUILD_MISSING = "--build missing" + sh(script: "sed -Ei 's, version = .*\"([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*, version = \"\\1-${env.BUILD_NUMBER}\",' conanfile.py") } } } - stage('Adjust for Testing/Stable') { - when { anyOf { - branch "${TESTING_BRANCH}" - branch "${STABLE_BRANCH}" - } } + stage('include build missing') { steps { script { - BUILD_MISSING = "" + BUILD_MISSING = "--build missing" } } } @@ -38,7 +31,7 @@ pipeline { steps { script { PROJECT = sh(script: "grep -m 1 'name =' conanfile.py | awk '{print \$3}' | tr -d '\n' | tr -d '\"'", returnStdout: true) - VER = sh(script: "grep -m 1 'version =' conanfile.py | awk '{print \$3}' | tr -d '\n' | tr -d '\"'", returnStdout: true) + VER = sh(script: "grep -m 1 ' version =' conanfile.py | awk '{print \$3}' | tr -d '\n' | tr -d '\"'", returnStdout: true) CONAN_CHANNEL = sh(script: "echo ${BRANCH_NAME} | sed -E 's,(\\w+-?\\d*)/.*,\\1,' | sed -E 's,-,_,' | tr -d '\n'", returnStdout: true) TAG = "${VER}@${CONAN_USER}/${CONAN_CHANNEL}" slackSend color: '#0063D1', channel: '#sds-ci', message: "*${PROJECT}/${TAG}* is building." @@ -55,148 +48,24 @@ pipeline { } } - stage('Build') { - failFast true - matrix { - agent { label 'sds-builder' } - axes { - axis { - name 'BUILD_TYPE' - values 'sanitize', 'debug', 'test' - } - axis { - name 'COVERAGE' - values 'False' - } - axis { - name 'PRERELEASE' - values 'True', 'False' - } - axis { - name 'ALLOC_IMPL' - values 'tcmalloc', 'libc' - } } - excludes { exclude { - axis { - name 'BUILD_TYPE' - values 'sanitize', 'test' - } - axis { - name 'COVERAGE' - values 'True' - } - } - exclude { - axis { - name 'PRERELEASE' - values 'False' - } - axis { - name 'BUILD_TYPE' - values 'debug', 'sanitize' - } - } - exclude { - axis { - name 'BUILD_TYPE' - values 'test' - } - axis { - name 'ALLOC_IMPL' - values 'libc' - } - } - exclude { - axis { - name 'BUILD_TYPE' - values 'sanitize' - } - axis { - name 'ALLOC_IMPL' - values 'tcmalloc' - } - } } - - stages { - stage('Adjust Tag for Master/PR') { - when { not { anyOf { - branch "${TESTING_BRANCH}" - branch "${STABLE_BRANCH}" - } } } - steps { - sh(script: "sed -Ei 's,version = .*\"([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*,version = \"\\1-${env.BUILD_NUMBER}\",' conanfile.py") - } - } - - stage("Build") { - when { allOf { - expression { "${COVERAGE}" == 'False' } - expression { "${BUILD_TYPE}" != 'sanitize' } - } } - steps { - sh "conan create ${BUILD_MISSING} -o prerelease=${PRERELEASE} -o malloc_impl=${ALLOC_IMPL} -pr ${BUILD_TYPE} . ${PROJECT}/${TAG}" - } - } - - stage("Sanitize") { - when { allOf { - expression { "${BUILD_TYPE}" == 'sanitize' } - } } - steps { - sh "conan create ${BUILD_MISSING} -o malloc_impl=${ALLOC_IMPL} -o sanitize=True -pr debug . ${PROJECT}/${TAG}" - } - } - - stage("Deploy") { - when { allOf { - expression { "${COVERAGE}" == 'False' } - expression { "${BUILD_TYPE}" != 'sanitize' } - expression { not { branch "PR_*" } } - } } - steps { - sh "conan user -r ebay-local -p ${ARTIFACTORY_PASS} _service_sds" - sh "conan upload ${PROJECT}/${TAG} -c --all -r ebay-local" - } - } - stage('Coverage') { - when { not { anyOf { - branch "${STABLE_BRANCH}" - expression { "${COVERAGE}" == 'False' } - } } } + stage("Compile") { + steps { + sh "conan export 3rd_party/folly folly/nu2.2023.12.11.00@ ; \ + conan create ${BUILD_MISSING} -pr debug -o ${PROJECT}:sanitize=True . ${PROJECT}/${TAG} ; \ + conan create ${BUILD_MISSING} -pr debug . ${PROJECT}/${TAG} ; \ + conan create ${BUILD_MISSING} -pr test -o ${PROJECT}:malloc_impl=tcmalloc . ${PROJECT}/${TAG} ; \ + conan create ${BUILD_MISSING} -pr test -o ${PROJECT}:prerelease=True -o ${PROJECT}:malloc_impl=tcmalloc . ${PROJECT}/${TAG} ; \ + " + } + } - stages { - stage("Adjust Sonar Branch") { - when { - not { - branch "${TARGET_BRANCH}" - } - } - steps { - sh "echo \"sonar.branch.target=${TARGET_BRANCH}\" >> sonar-project.properties" - } - } - stage("Code Coverage") { - steps { - slackSend channel: '#sds-ci', message: "*${PROJECT}:${TAG}* is undergoing Code Coverage." - sh "echo \"sonar.branch.name=${BRANCH_NAME}\" >> sonar-project.properties" - sh "conan install -pr ${BUILD_TYPE} ${BUILD_MISSING} -o ${PROJECT}:coverage=True ." - sh "build-wrapper-linux-x86-64 --out-dir /tmp/sonar conan build ." - sh "find . -name \"*.gcno\" -exec gcov {} \\;" - withSonarQubeEnv('sds-sonar') { - sh "sonar-scanner -Dsonar.projectBaseDir=. -Dsonar.projectVersion=\"${VER}\"" - } - } - } - stage("Quality Gate") { - steps { - timeout(time: 30, unit: 'MINUTES') { - waitForQualityGate abortPipeline: true - } - } - } - } - } - } + stage("Deploy") { + when { + expression { !(env.BRANCH_NAME =~ /PR-/) } + } + steps { + sh "conan user -r ebay-local -p ${ARTIFACTORY_PASS} _service_sds" + sh "conan upload ${PROJECT}/${TAG} --parallel -c --all -r ebay-local" } } } diff --git a/3rd_party/folly/conan_deps.cmake b/3rd_party/folly/conan_deps.cmake new file mode 100644 index 00000000..bdc0907f --- /dev/null +++ b/3rd_party/folly/conan_deps.cmake @@ -0,0 +1,39 @@ +# Set the dependency flags expected by https://github.com/facebook/folly/blob/v2023.12.18.00/CMake/folly-deps.cmake + +macro(custom_find_package name var) + find_package(${name} ${ARGN} + # Allow only Conan packages + NO_DEFAULT_PATH + PATHS ${CMAKE_PREFIX_PATH} + ) + set(${var}_FOUND TRUE) + set(${var}_VERSION ${${name}_VERSION}) + set(${var}_VERSION_STRING ${${name}_VERSION_STRING}) + set(${var}_INCLUDE_DIRS ${${name}_INCLUDE_DIRS}) + set(${var}_INCLUDE_DIR ${${name}_INCLUDE_DIR}) + set(${var}_INCLUDE ${${name}_INCLUDE_DIR}) + set(${var}_LIB ${${name}_LIBRARIES}) + set(${var}_LIBRARY ${${name}_LIBRARIES}) + set(${var}_LIBRARIES ${${name}_LIBRARIES}) + set(${var}_DEFINITIONS ${${name}_DEFINITIONS}) +endmacro() + +custom_find_package(BZip2 BZIP2) +custom_find_package(Backtrace BACKTRACE) +custom_find_package(DoubleConversion DOUBLE_CONVERSION REQUIRED) +custom_find_package(Gflags LIBGFLAGS) +custom_find_package(Glog GLOG) +custom_find_package(LZ4 LZ4) +custom_find_package(LibAIO LIBAIO) +custom_find_package(LibDwarf LIBDWARF) +custom_find_package(LibEvent LIBEVENT REQUIRED) +custom_find_package(LibLZMA LIBLZMA) +custom_find_package(LibUnwind LIBUNWIND) +custom_find_package(LibUring LIBURING) +custom_find_package(Libiberty LIBIBERTY) +custom_find_package(Libsodium LIBSODIUM) +custom_find_package(OpenSSL OPENSSL REQUIRED) +custom_find_package(Snappy SNAPPY) +custom_find_package(ZLIB ZLIB) +custom_find_package(Zstd ZSTD) +custom_find_package(fmt FMT REQUIRED) diff --git a/3rd_party/folly/conandata.yml b/3rd_party/folly/conandata.yml new file mode 100644 index 00000000..93d9352f --- /dev/null +++ b/3rd_party/folly/conandata.yml @@ -0,0 +1,19 @@ +sources: + "nu2.2023.12.18.00": + url: "https://github.com/facebook/folly/releases/download/v2023.12.18.00/folly-v2023.12.18.00.tar.gz" + sha256: "57ce880e3ae7b4d4fe0980be64da9e6ca7dd09e2de477670bf984e11cf7739f2" + "2022.10.31.00": + url: "https://github.com/facebook/folly/releases/download/v2022.10.31.00/folly-v2022.10.31.00.tar.gz" + sha256: "d7749f78eee2a327c1fa6b4a290e4bcd7115cdd7f7ef59f9e043ed59e597ab30" +patches: + "nu2.2023.12.18.00": + - patch_file: "patches/2023-001-compiler-flags.patch" + patch_description: "Do not hard-code debug flag for all build types" + patch_type: "conan" + - patch_file: "patches/2023-002-timespec.patch" + patch_description: "Fix liburing inclusion of timespec" + patch_type: "conan" + "2022.10.31.00": + - patch_file: "patches/2022-001-compiler-flags.patch" + patch_description: "Do not hard-code debug flag for all build types" + patch_type: "conan" diff --git a/3rd_party/folly/conanfile.py b/3rd_party/folly/conanfile.py new file mode 100755 index 00000000..a924d3b7 --- /dev/null +++ b/3rd_party/folly/conanfile.py @@ -0,0 +1,350 @@ +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.apple import is_apple_os +from conan.tools.build import can_run, check_min_cppstd +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout +from conan.tools.files import apply_conandata_patches, export_conandata_patches, get, copy, rmdir, replace_in_file, save +from conan.tools.microsoft import is_msvc, msvc_runtime_flag +from conan.tools.scm import Version +import os + +required_conan_version = ">=1.53.0" + + +class FollyConan(ConanFile): + name = "folly" + description = "An open-source C++ components library developed and used at Facebook" + topics = ("facebook", "components", "core", "efficiency") + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://github.com/facebook/folly" + license = "Apache-2.0" + + package_type = "library" + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + "use_sse4_2": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + "use_sse4_2": False + } + + @property + def _min_cppstd(self): + return 17 + + @property + def _compilers_minimum_version(self): + return { + "gcc": "7", + "Visual Studio": "16", + "msvc": "192", + "clang": "6", + "apple-clang": "10", + } + + def export_sources(self): + export_conandata_patches(self) + copy(self, "conan_deps.cmake", self.recipe_folder, os.path.join(self.export_sources_folder, "src")) + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + if str(self.settings.arch) not in ["x86", "x86_64"]: + del self.options.use_sse4_2 + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + + def layout(self): + cmake_layout(self, src_folder="src") + + def requirements(self): + self.requires("boost/1.83.0", transitive_headers=True, transitive_libs=True) + self.requires("bzip2/1.0.8") + self.requires("double-conversion/3.3.0", transitive_headers=True, transitive_libs=True) + self.requires("gflags/2.2.2") + self.requires("glog/0.6.0", transitive_headers=True, transitive_libs=True) + self.requires("libevent/2.1.12", transitive_headers=True, transitive_libs=True) + self.requires("openssl/[>=1.1 <4]") + self.requires("lz4/1.9.4", transitive_libs=True) + self.requires("snappy/1.1.10") + self.requires("zlib/[>=1.2.11 <2]") + self.requires("liburing/[>=2.1]") + self.requires("zstd/1.5.5", transitive_libs=True) + if not is_msvc(self): + self.requires("libdwarf/20191104") + self.requires("libsodium/1.0.19") + self.requires("xz_utils/5.4.5") + # FIXME: Causing compilation issues on clang: self.requires("jemalloc/5.2.1") + if self.settings.os in ["Linux", "FreeBSD"]: + self.requires("libiberty/9.1.0") + self.requires("libunwind/1.7.2") + self.requires("fmt/10.2.1", transitive_headers=True, transitive_libs=True) + + @property + def _required_boost_components(self): + return ["context", "filesystem", "program_options", "regex", "system", "thread"] + + @property + def _required_boost_conan_components(self): + return [f"boost::{comp}" for comp in self._required_boost_components] + + @property + def _required_boost_cmake_targets(self): + return [f"Boost::{comp}" for comp in self._required_boost_components] + + def validate(self): + if self.settings.compiler.cppstd: + check_min_cppstd(self, self._min_cppstd) + minimum_version = self._compilers_minimum_version.get(str(self.settings.compiler), False) + if minimum_version and Version(self.settings.compiler.version) < minimum_version: + raise ConanInvalidConfiguration( + f"{self.ref} requires C++{self._min_cppstd}, which your compiler does not support." + ) + + if is_apple_os(self) and self.settings.arch != "x86_64": + raise ConanInvalidConfiguration("Conan currently requires a 64bit target architecture for Folly on Macos") + + if is_apple_os(self): + raise ConanInvalidConfiguration("Current recipe doesn't support Macos. Contributions are welcome.") + + if self.settings.os == "Windows" and self.settings.arch != "x86_64": + raise ConanInvalidConfiguration("Folly requires a 64bit target architecture on Windows") + + if (is_apple_os(self) or self.settings.os == "Windows") and self.options.shared: + raise ConanInvalidConfiguration(f"Folly could not be built on {self.settings.os} as shared library") + + if self.settings.os == "Windows": + raise ConanInvalidConfiguration(f"{self.ref} could not be built on {self.settings.os}. PR's are welcome.") + + if self.settings.compiler == "clang" and self.options.shared: + raise ConanInvalidConfiguration(f"Folly {self.version} could not be built by clang as a shared library") + + glog = self.dependencies["glog"] + if self.options.shared and not glog.options.shared: + raise ConanInvalidConfiguration(f"If Folly is built as shared lib, glog must be a shared lib too.") + + boost = self.dependencies["boost"] + if boost.options.header_only: + raise ConanInvalidConfiguration("Folly could not be built with a header only Boost") + + miss_boost_required_comp = any(getattr(boost.options, f"without_{boost_comp}", True) for boost_comp in self._required_boost_components) + if miss_boost_required_comp: + required_components = ", ".join(self._required_boost_components) + raise ConanInvalidConfiguration(f"Folly requires these boost components: {required_components}") + + if self.options.get_safe("use_sse4_2") and str(self.settings.arch) not in ['x86', 'x86_64']: + raise ConanInvalidConfiguration(f"{self.ref} can use the option use_sse4_2 only on x86 and x86_64 archs.") + + def source(self): + get(self, **self.conan_data["sources"][self.version], strip_root=False) + + def _cppstd_flag_value(self, cppstd): + cppstd = str(cppstd) + if cppstd.startswith("gnu"): + prefix = "gnu" + year = cppstd[3:] + else: + prefix = "c" + year = cppstd + if is_msvc(self): + prefix = "" + if year > "17": + year = "latest" + return f"{prefix}++{year}" + + def generate(self): + tc = CMakeToolchain(self) + + tc.cache_variables["CMAKE_PROJECT_folly_INCLUDE"] = os.path.join(self.source_folder, "conan_deps.cmake") + + if can_run(self): + for var in ["FOLLY_HAVE_UNALIGNED_ACCESS", "FOLLY_HAVE_LINUX_VDSO", "FOLLY_HAVE_WCHAR_SUPPORT", "HAVE_VSNPRINTF_ERRORS"]: + tc.variables[f"{var}_EXITCODE"] = "0" + tc.variables[f"{var}_EXITCODE__TRYRUN_OUTPUT"] = "" + + if self.options.get_safe("use_sse4_2") and str(self.settings.arch) in ["x86", "x86_64"]: + tc.preprocessor_definitions["FOLLY_SSE"] = "4" + tc.preprocessor_definitions["FOLLY_SSE_MINOR"] = "2" + if not is_msvc(self): + cflags = "-mfma" + else: + cflags = "/arch:FMA" + tc.blocks["cmake_flags_init"].template += ( + f'string(APPEND CMAKE_CXX_FLAGS_INIT " {cflags}")\n' + f'string(APPEND CMAKE_C_FLAGS_INIT " {cflags}")\n' + ) + + # Folly is not respecting this from the helper https://github.com/conan-io/conan-center-index/pull/15726/files#r1097068754 + tc.variables["CMAKE_POSITION_INDEPENDENT_CODE"] = self.options.get_safe("fPIC", True) + # Relocatable shared lib on Macos + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0042"] = "NEW" + # Honor CMAKE_REQUIRED_LIBRARIES in check_include_file_xxx + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0075"] = "NEW" + # Honor BUILD_SHARED_LIBS from conan_toolchain (see https://github.com/conan-io/conan/issues/11840) + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0077"] = "NEW" + # Honor Boost_ROOT set by boost recipe + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0074"] = "NEW" + + cxx_std_value = self._cppstd_flag_value(self.settings.get_safe("compiler.cppstd", self._min_cppstd)) + # 2019.10.21.00 -> either MSVC_ flags or CXX_STD + if is_msvc(self): + tc.variables["MSVC_LANGUAGE_VERSION"] = cxx_std_value + tc.variables["MSVC_ENABLE_ALL_WARNINGS"] = False + tc.variables["MSVC_USE_STATIC_RUNTIME"] = "MT" in msvc_runtime_flag(self) + tc.preprocessor_definitions["NOMINMAX"] = "" + else: + tc.variables["CXX_STD"] = cxx_std_value + + if not self.dependencies["boost"].options.header_only: + tc.cache_variables["BOOST_LINK_STATIC"] = not self.dependencies["boost"].options.shared + + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0074"] = "NEW" # Honor Boost_ROOT set by boost recipe + tc.generate() + + deps = CMakeDeps(self) + # deps.set_property("backtrace", "cmake_file_name", "Backtrace") + deps.set_property("boost", "cmake_file_name", "Boost") + deps.set_property("bzip2", "cmake_file_name", "BZip2") + deps.set_property("double-conversion", "cmake_file_name", "DoubleConversion") + deps.set_property("fmt", "cmake_file_name", "fmt") + deps.set_property("gflags", "cmake_file_name", "Gflags") + deps.set_property("glog", "cmake_file_name", "Glog") + # deps.set_property("libaio", "cmake_file_name", "LibAIO") + deps.set_property("libdwarf", "cmake_file_name", "LibDwarf") + deps.set_property("libevent", "cmake_file_name", "LibEvent") + deps.set_property("libiberty", "cmake_file_name", "Libiberty") + deps.set_property("libsodium", "cmake_file_name", "Libsodium") + deps.set_property("libunwind", "cmake_file_name", "LibUnwind") + deps.set_property("liburing", "cmake_file_name", "LibUring") + deps.set_property("lz4", "cmake_file_name", "LZ4") + deps.set_property("openssl", "cmake_file_name", "OpenSSL") + deps.set_property("snappy", "cmake_file_name", "Snappy") + deps.set_property("xz_utils", "cmake_file_name", "LibLZMA") + deps.set_property("zlib", "cmake_file_name", "ZLIB") + deps.set_property("zstd", "cmake_file_name", "Zstd") + deps.generate() + + def _patch_sources(self): + apply_conandata_patches(self) + folly_deps = os.path.join(self.source_folder, "CMake", "folly-deps.cmake") + replace_in_file(self, folly_deps, " MODULE", " ") + replace_in_file(self, folly_deps, "${Boost_LIBRARIES}", f"{' '.join(self._required_boost_cmake_targets)}") + replace_in_file(self, folly_deps, "OpenSSL 1.1.1", "OpenSSL") + # Disable example + save(self, os.path.join(self.source_folder, "folly", "logging", "example", "CMakeLists.txt"), "") + + def build(self): + self._patch_sources() + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + copy(self, pattern="LICENSE", dst=os.path.join(self.package_folder, "licenses"), src=self.source_folder) + cmake = CMake(self) + cmake.install() + rmdir(self, os.path.join(self.package_folder, "lib", "cmake")) + rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "folly") + self.cpp_info.set_property("cmake_target_name", "folly::folly") + self.cpp_info.set_property("pkg_config_name", "libfolly") + + self.cpp_info.components["libfolly"].set_property("cmake_target_name", "Folly::folly") + self.cpp_info.components["libfolly"].set_property("pkg_config_name", "libfolly") + self.cpp_info.components["libfolly"].libs = ["folly"] + self.cpp_info.components["libfolly"].requires = ["fmt::fmt"] + self._required_boost_conan_components + [ + "double-conversion::double-conversion", + "gflags::gflags", + "glog::glog", + "libevent::libevent", + "lz4::lz4", + "openssl::openssl", + "bzip2::bzip2", + "snappy::snappy", + "liburing::liburing", + "zlib::zlib", + "zstd::zstd", + "libsodium::libsodium", + "xz_utils::xz_utils" + ] + if not is_msvc(self): + self.cpp_info.components["libfolly"].requires.append("libdwarf::libdwarf") + if self.settings.os in ["Linux", "FreeBSD"]: + self.cpp_info.components["libfolly"].requires.extend(["libiberty::libiberty", "libunwind::libunwind"]) + self.cpp_info.components["libfolly"].system_libs.extend(["pthread", "dl", "rt"]) + self.cpp_info.components["libfolly"].defines.extend(["FOLLY_HAVE_ELF", "FOLLY_HAVE_DWARF"]) + elif self.settings.os == "Windows": + self.cpp_info.components["libfolly"].system_libs.extend(["ws2_32", "iphlpapi", "crypt32"]) + + if str(self.settings.compiler.libcxx) == "libstdc++" or ( + self.settings.compiler == "apple-clang" and + Version(self.settings.compiler.version.value) == "9.0" and + self.settings.compiler.libcxx == "libc++"): + self.cpp_info.components["libfolly"].system_libs.append("atomic") + + if self.settings.compiler == "apple-clang" and Version(self.settings.compiler.version.value) >= "11.0": + self.cpp_info.components["libfolly"].system_libs.append("c++abi") + + if self.settings.compiler == "gcc" and Version(self.settings.compiler.version) < "9": + self.cpp_info.components["libfolly"].system_libs.append("stdc++fs") + + if self.settings.compiler == "clang" and Version(self.settings.compiler.version) < "9": + self.cpp_info.components["libfolly"].system_libs.append("stdc++fs" if self.settings.compiler.libcxx in ["libstdc++", "libstdc++11"] else "c++fs") + + self.cpp_info.components["follybenchmark"].set_property("cmake_target_name", "Folly::follybenchmark") + self.cpp_info.components["follybenchmark"].set_property("pkg_config_name", "libfollybenchmark") + self.cpp_info.components["follybenchmark"].libs = ["follybenchmark"] + self.cpp_info.components["follybenchmark"].requires = ["libfolly"] + + self.cpp_info.components["folly_test_util"].set_property("cmake_target_name", "Folly::folly_test_util") + self.cpp_info.components["folly_test_util"].set_property("pkg_config_name", "libfolly_test_util") + self.cpp_info.components["folly_test_util"].libs = ["folly_test_util"] + self.cpp_info.components["folly_test_util"].requires = ["libfolly"] + + if self.settings.os in ["Linux", "FreeBSD"]: + self.cpp_info.components["folly_exception_tracer_base"].set_property("cmake_target_name", "Folly::folly_exception_tracer_base") + self.cpp_info.components["folly_exception_tracer_base"].set_property("pkg_config_name", "libfolly_exception_tracer_base") + self.cpp_info.components["folly_exception_tracer_base"].libs = ["folly_exception_tracer_base"] + self.cpp_info.components["folly_exception_tracer_base"].requires = ["libfolly"] + + self.cpp_info.components["folly_exception_tracer"].set_property("cmake_target_name", "Folly::folly_exception_tracer") + self.cpp_info.components["folly_exception_tracer"].set_property("pkg_config_name", "libfolly_exception_tracer") + self.cpp_info.components["folly_exception_tracer"].libs = ["folly_exception_tracer"] + self.cpp_info.components["folly_exception_tracer"].requires = ["folly_exception_tracer_base"] + + self.cpp_info.components["folly_exception_counter"].set_property("cmake_target_name", "Folly::folly_exception_counter") + self.cpp_info.components["folly_exception_counter"].set_property("pkg_config_name", "libfolly_exception_counter") + self.cpp_info.components["folly_exception_counter"].libs = ["folly_exception_counter"] + self.cpp_info.components["folly_exception_counter"].requires = ["folly_exception_tracer"] + + # TODO: to remove in conan v2 once cmake_find_package_* & pkg_config generators removed + self.cpp_info.filenames["cmake_find_package"] = "folly" + self.cpp_info.filenames["cmake_find_package_multi"] = "folly" + self.cpp_info.names["cmake_find_package"] = "Folly" + self.cpp_info.names["cmake_find_package_multi"] = "Folly" + self.cpp_info.names["pkg_config"] = "libfolly" + self.cpp_info.components["libfolly"].names["cmake_find_package"] = "folly" + self.cpp_info.components["libfolly"].names["cmake_find_package_multi"] = "folly" + + # TODO: to remove in conan v2 once cmake_find_package_* & pkg_config generators removed + self.cpp_info.components["follybenchmark"].names["cmake_find_package"] = "follybenchmark" + self.cpp_info.components["follybenchmark"].names["cmake_find_package_multi"] = "follybenchmark" + self.cpp_info.components["folly_test_util"].names["cmake_find_package"] = "folly_test_util" + self.cpp_info.components["folly_test_util"].names["cmake_find_package_multi"] = "folly_test_util" + + if self.settings.os in ["Linux", "FreeBSD"]: + # TODO: to remove in conan v2 once cmake_find_package_* & pkg_config generators removed + self.cpp_info.components["folly_exception_tracer_base"].names["cmake_find_package"] = "folly_exception_tracer_base" + self.cpp_info.components["folly_exception_tracer_base"].names["cmake_find_package_multi"] = "folly_exception_tracer_base" + self.cpp_info.components["folly_exception_tracer"].names["cmake_find_package"] = "folly_exception_tracer" + self.cpp_info.components["folly_exception_tracer"].names["cmake_find_package_multi"] = "folly_exception_tracer" + self.cpp_info.components["folly_exception_counter"].names["cmake_find_package"] = "folly_exception_counter" + self.cpp_info.components["folly_exception_counter"].names["cmake_find_package_multi"] = "folly_exception_counter" diff --git a/3rd_party/folly/patches/2022-001-compiler-flags.patch b/3rd_party/folly/patches/2022-001-compiler-flags.patch new file mode 100644 index 00000000..f7894a5b --- /dev/null +++ b/3rd_party/folly/patches/2022-001-compiler-flags.patch @@ -0,0 +1,21 @@ +--- CMake/FollyCompilerUnix.cmake ++++ CMake/FollyCompilerUnix.cmake +@@ -28,9 +28,9 @@ set( + ) + mark_as_advanced(CXX_STD) + +-set(CMAKE_CXX_FLAGS_COMMON "-g -Wall -Wextra") ++set(CMAKE_CXX_FLAGS_COMMON "-Wall -Wextra") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_COMMON}") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${CMAKE_CXX_FLAGS_COMMON} -O3") + + # Note that CMAKE_REQUIRED_FLAGS must be a string, not a list + set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=${CXX_STD}") +@@ -43,7 +43,6 @@ function(apply_folly_compile_options_to_target THETARGET) + ) + target_compile_options(${THETARGET} + PRIVATE +- -g + -std=${CXX_STD} + -finput-charset=UTF-8 + -fsigned-char diff --git a/3rd_party/folly/patches/2023-001-compiler-flags.patch b/3rd_party/folly/patches/2023-001-compiler-flags.patch new file mode 100644 index 00000000..728a3771 --- /dev/null +++ b/3rd_party/folly/patches/2023-001-compiler-flags.patch @@ -0,0 +1,19 @@ +--- CMake/FollyCompilerUnix.cmake ++++ CMake/FollyCompilerUnix.cmake +@@ -12,7 +12,7 @@ + # See the License for the specific language governing permissions and + # limitations under the License. + +-set(CMAKE_CXX_FLAGS_COMMON "-g -Wall -Wextra") ++set(CMAKE_CXX_FLAGS_COMMON "-Wall -Wextra") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_COMMON}") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${CMAKE_CXX_FLAGS_COMMON} -O3") + +@@ -25,7 +25,6 @@ + ) + target_compile_options(${THETARGET} + PRIVATE +- -g + -finput-charset=UTF-8 + -fsigned-char + -Wall diff --git a/3rd_party/folly/patches/2023-002-timespec.patch b/3rd_party/folly/patches/2023-002-timespec.patch new file mode 100644 index 00000000..974a120d --- /dev/null +++ b/3rd_party/folly/patches/2023-002-timespec.patch @@ -0,0 +1,38 @@ +diff -Naur a/folly/io/async/AsyncSocket.cpp b/folly/io/async/AsyncSocket.cpp +--- a/folly/io/async/AsyncSocket.cpp 2023-12-08 20:38:13.000000000 -0700 ++++ b/folly/io/async/AsyncSocket.cpp 2023-12-12 10:15:06.023030521 -0700 +@@ -18,6 +18,9 @@ + + #include + ++/* for struct sock_extended_err*/ ++#include ++ + #include + #include + #include +diff -Naur a/folly/io/async/AsyncUDPSocket.cpp b/folly/io/async/AsyncUDPSocket.cpp +--- a/folly/io/async/AsyncUDPSocket.cpp 2023-12-08 20:38:13.000000000 -0700 ++++ b/folly/io/async/AsyncUDPSocket.cpp 2023-12-12 10:19:06.419424565 -0700 +@@ -17,6 +17,9 @@ + #include + #include + ++/* for struct sock_extended_err*/ ++#include ++ + #include + + #include +diff -Naur a/folly/net/NetOps.h b/folly/net/NetOps.h +--- a/folly/net/NetOps.h 2023-12-12 10:16:10.675139766 -0700 ++++ b/folly/net/NetOps.h 2023-12-12 10:15:55.087113425 -0700 +@@ -114,7 +114,7 @@ + #endif + #endif + /* for struct sock_extended_err*/ +-#include ++#include + #endif + #endif + diff --git a/3rd_party/userspace-rcu/conandata.yml b/3rd_party/userspace-rcu/conandata.yml new file mode 100644 index 00000000..9243c443 --- /dev/null +++ b/3rd_party/userspace-rcu/conandata.yml @@ -0,0 +1,4 @@ +sources: + "nu2.0.14.0": + url: "https://github.com/urcu/userspace-rcu/archive/refs/tags/v0.14.0.tar.gz" + sha256: "42fb5129a3fffe5a4b790dfe1ea3a734c69ee095fefbf649326269bba94c262d" diff --git a/3rd_party/userspace-rcu/conanfile.py b/3rd_party/userspace-rcu/conanfile.py new file mode 100644 index 00000000..bc82bbc3 --- /dev/null +++ b/3rd_party/userspace-rcu/conanfile.py @@ -0,0 +1,87 @@ +import os + +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.env import VirtualBuildEnv +from conan.tools.files import chdir, copy, get, rm, rmdir +from conan.tools.gnu import Autotools, AutotoolsToolchain +from conan.tools.layout import basic_layout + +required_conan_version = ">=1.53.0" + + +class UserspaceRCUConan(ConanFile): + name = "userspace-rcu" + description = "Userspace RCU (read-copy-update) library" + license = "LGPL-2.1" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://liburcu.org/" + topics = "urcu" + + package_type = "library" + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + } + + def configure(self): + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + if self.options.shared: + self.options.rm_safe("fPIC") + + def layout(self): + basic_layout(self, src_folder="src") + + def validate(self): + if self.settings.os not in ["Linux", "FreeBSD", "Macos"]: + raise ConanInvalidConfiguration(f"Building for {self.settings.os} unsupported") + if self.version == "0.11.4" and self.settings.compiler == "apple-clang": + # Fails with "cds_hlist_add_head_rcu.c:19:10: fatal error: 'urcu/urcu-memb.h' file not found" + raise ConanInvalidConfiguration(f"{self.ref} is not compatible with apple-clang") + + def build_requirements(self): + self.tool_requires("libtool/2.4.7") + + def source(self): + get(self, **self.conan_data["sources"][self.version], strip_root=True) + + def generate(self): + env = VirtualBuildEnv(self) + env.generate() + tc = AutotoolsToolchain(self) + tc.generate() + + def build(self): + autotools = Autotools(self) + autotools.autoreconf() + autotools.configure() + autotools.make() + + def package(self): + copy(self, "LICENSE*", + src=self.source_folder, + dst=os.path.join(self.package_folder, "licenses")) + autotools = Autotools(self) + autotools.install() + + rm(self, "*.la", self.package_folder, recursive=True) + rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) + rmdir(self, os.path.join(self.package_folder, "share")) + + def package_info(self): + for lib_type in ["", "-bp", "-cds", "-mb", "-memb", "-qsbr", "-signal"]: + component_name = f"urcu{lib_type}" + self.cpp_info.components[component_name].libs = ["urcu-common", component_name] + self.cpp_info.components[component_name].set_property("pkg_config_name", component_name) + if self.settings.os in ["Linux", "FreeBSD"]: + self.cpp_info.components[component_name].system_libs = ["pthread"] + + # Some definitions needed for MB and Signal variants + self.cpp_info.components["urcu-mb"].defines = ["RCU_MB"] + self.cpp_info.components["urcu-signal"].defines = ["RCU_SIGNAL"] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c3d5f659 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Dependency Changes + +- Updated: cpr/1.9.3 +- Updated: pistache/0.0.5 +- Updated: prometheus-cpp/1.1.0 + +## [8.x] + +### Changed + +- Moved SISL code to github.com; start Changelog + +[Unreleased]: https://github.com/eBay/sisl/compare/stable/v8.x...HEAD +[8.x]: https://github.com/eBay/sisl/compare/v5.0.10...stable/v8.x diff --git a/CMakeLists.txt b/CMakeLists.txt index 632d675c..52d0d67c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) project (sisl) option(DEBUG_CMAKE "Debug CMake messages option" OFF) @@ -9,17 +9,24 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) # turn on folder hierarchies include (cmake/Flags.cmake) set(CMAKE_CXX_STANDARD 20) -enable_testing() -if(EXISTS ${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - conan_basic_setup(TARGETS) -else () - message("The file conanbuildinfo.cmake doesn't exist, some properties will be unavailable") -endif () +if (NOT BUILD_TESTING STREQUAL OFF) + set(ENABLE_TESTING ON) + enable_testing() + find_package(GTest QUIET REQUIRED) +endif() + +if (DEFINED BUILD_COVERAGE) + if (${BUILD_COVERAGE}) + include (cmake/CodeCoverage.cmake) + APPEND_COVERAGE_COMPILER_FLAGS() + SETUP_TARGET_FOR_COVERAGE_GCOVR_XML(NAME coverage EXECUTABLE ctest DEPENDENCIES ) + endif() +endif() -if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") - include (cmake/debug_flags.cmake) +if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") + message(STATUS "Debug build") + add_flags("-D_DEBUG") endif() if (DEFINED MALLOC_IMPL) @@ -49,20 +56,29 @@ if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows") set(CMAKE_THREAD_PREFER_PTHREAD TRUE) endif() -find_package(benchmark REQUIRED) -find_package(Boost REQUIRED) -find_package(cpr REQUIRED) -find_package(cxxopts REQUIRED) -find_package(folly REQUIRED) -find_package(GTest REQUIRED) -find_package(jwt-cpp REQUIRED) -find_package(nlohmann_json REQUIRED) -find_package(prerelease_dummy QUIET) -find_package(prometheus-cpp REQUIRED) -find_package(zmarok-semver REQUIRED) -find_package(spdlog REQUIRED) -find_package(Threads REQUIRED) -find_package(userspace-rcu REQUIRED) +find_package(Benchmark QUIET REQUIRED) +find_package(Boost QUIET REQUIRED) +find_package(cxxopts QUIET REQUIRED) +if (${MALLOC_IMPL} STREQUAL "tcmalloc") + find_package(gperftools QUIET REQUIRED) +endif() + +if (${MALLOC_IMPL} STREQUAL "jemalloc") + find_package(jemalloc QUIET REQUIRED) +endif() + +find_package(folly) +find_package(Boost) +find_package(breakpad) +find_package(cxxopts) +find_package(flatbuffers) +find_package(gRPC) +find_package(nlohmann_json) +find_package(prometheus-cpp) +find_package(userspace-rcu) +find_package(spdlog) +find_package(zmarok-semver) +find_package(benchmark) list (APPEND COMMON_DEPS Boost::headers @@ -70,12 +86,28 @@ list (APPEND COMMON_DEPS nlohmann_json::nlohmann_json prometheus-cpp::prometheus-cpp spdlog::spdlog - userspace-rcu::userspace-rcu ) +if(${breakpad_FOUND}) + list (APPEND COMMON_DEPS breakpad::breakpad) +endif() +if(${userspace-rcu_FOUND}) + list (APPEND COMMON_DEPS userspace-rcu::userspace-rcu) +endif() + if (${prerelease_dummy_FOUND}) list (APPEND COMMON_DEPS prerelease_dummy::prerelease_dummy) endif () +if (DEFINED MALLOC_IMPL) + if (${MALLOC_IMPL} STREQUAL "tcmalloc") + list(APPEND COMMON_DEPS gperftools::gperftools) + endif() + + if (${MALLOC_IMPL} STREQUAL "jemalloc") + list(APPEND COMMON_DEPS jemalloc::jemalloc) + endif() +endif() + find_program(CCACHE_FOUND ccache) if (CCACHE_FOUND) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) @@ -92,12 +124,12 @@ endif() # add conan information add_flags("-DPACKAGE_NAME=sisl") -if (DEFINED CONAN_PACKAGE_VERSION) - message("Package Version: [${CONAN_PACKAGE_VERSION}]") - add_flags("-DPACKAGE_VERSION=\\\"${CONAN_PACKAGE_VERSION}\\\"") +if (DEFINED PACKAGE_VERSION) + message("Package Version: [${PACKAGE_VERSION}]") + add_flags("-DPACKAGE_VERSION=\\\"${PACKAGE_VERSION}\\\"") else () message("Unknown Package Version") - add_flags("-DPACKAGE_VERSION=\\\"${CONAN_PACKAGE_VERSION}\\\"") + add_flags("-DPACKAGE_VERSION=\\\"unknown\\\"") endif () if(UNIX) @@ -105,43 +137,11 @@ if(UNIX) add_flags("-D_POSIX_C_SOURCE=200809L -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE") endif() -include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/src/auth_manager) -include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/src/settings) - -if (DEFINED EVHTP_ON) - if (${EVHTP_ON}) - add_subdirectory (src/async_http) - endif() -endif() +include_directories(BEFORE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) -#add_subdirectory (src/btree) -add_subdirectory (src/cache) -add_subdirectory (src/logging) -add_subdirectory (src/options) -add_subdirectory (src/wisr) -add_subdirectory (src/metrics) -add_subdirectory (src/fds) -add_subdirectory (src/settings) -add_subdirectory (src/utility) -add_subdirectory (src/sisl_version) -add_subdirectory (src/auth_manager) -add_subdirectory (src/file_watcher) - -add_library(sisl - $ - $ - $ - $ - $ - $ - $ - $ - $ - $ - ) -target_link_libraries(sisl - Folly::Folly - ) +add_subdirectory(src) # build info string(TOUPPER "${CMAKE_BUILD_TYPE}" UC_CMAKE_BUILD_TYPE) diff --git a/README.md b/README.md index 425ddde1..95711f83 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # SymbiosisLib (sisl) -[![Conan Build](https://github.com/eBay/sisl/actions/workflows/build_with_conan.yml/badge.svg?branch=master)](https://github.com/eBay/sisl/actions/workflows/build_with_conan.yml) +[![Conan Build](https://github.com/eBay/sisl/actions/workflows/merge_build.yml/badge.svg?branch=master)](https://github.com/eBay/sisl/actions/workflows/merge_build.yml) +[![CodeCov](https://codecov.io/gh/eBay/sisl/branch/master/graph/badge.svg)](https://codecov.io/gh/eBay/Sisl) This repo provides a symbiosis of libraries (thus named sisl - pronounced like sizzle) mostly for very high performance data structures and utilities. This is mostly on top of folly, boost, STL and other good well known libraries. Thus its not trying @@ -12,11 +13,6 @@ to replace these libraries, but provide a layer on top of it. In general there a Following are the tools it provides so far ## Whats in this library -### Async HTTP Server - -Provides an HTTP REST Server for asynchronous programming model. It works on top of evhtp library, but wraps threading model -C++ methods for evhtp C library. - ### Metrics A very high performance metrics collection (counters, histograms and gauges) and report the results in form of json or @@ -24,6 +20,8 @@ sent to prometheus whichever caller choose from. It is meant to scale with multi metrics. The collection is extremely fast <<5ns per metric, but pay penalty during metrics result gathering which is rare. It uses Wisr framework which will be detailed next +*Lacks MacOS support* + ### Wisr WISR stands for Waitfree Inserts Snoozy Rest. This is a framework and data structures on top of this framework which provides @@ -36,7 +34,9 @@ More details in the Wisr README under [src/wisr/README.md] ### FDS This is a bunch of data structures meant for high performance or specific use cases. Each of these structures are detailed in their -corresponding source files. Some of the major data structures are +corresponding source files. Some of the major data structures are listed below: + +*Lacks MacOS support* #### Bitset A high performance bitset to have various functionalities to scan the contiguous 1s, 0s, set/reset multiple bits without iterating over @@ -61,6 +61,9 @@ Capture the vector in a pool in thread local fashion, so that vectors are not bu ### Settings Framework Please refer to the README under [src/settings/README.md] +### Flip +Flip is fault injection framework, Please refer to the README under [src/flip/README.md] + ## Installation This is mostly header only library and can be just compiled into your code. There are some of the pieces which needs a library (libsisl) to be built. @@ -69,19 +72,16 @@ to be built. Assuming the conan setup is already done ``` -$ mkdir build -$ cd build +$ ./prepare.sh # this will export some recipes to the conan cache +$ // ./prepare_v2.sh for conan >= 2.0 # Install all dependencies -$ conan install .. +$ conan install . -# Build the libsisl.a -$ conan build .. +# Build and Run Tests +$ conan build . ``` -### Without conan -To be Added - ## Contributing to This Project We welcome contributions. If you find any bugs, potential flaws and edge cases, improvements, new feature suggestions or discussions, please submit issues or pull requests. @@ -92,7 +92,8 @@ Harihara Kadayam hkadayam@ebay.com Copyright 2021 eBay Inc. Primary Author: Harihara Kadayam -Primary Developers: Harihara Kadayam, Rishabh Mittal, Bryan Zimmerman, Brian Szymd + +Primary Developers: Harihara Kadayam, Rishabh Mittal, Bryan Zimmerman, Brian Szmyd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0. diff --git a/cmake/debug_flags.cmake b/cmake/debug_flags.cmake deleted file mode 100644 index 45c1e7f3..00000000 --- a/cmake/debug_flags.cmake +++ /dev/null @@ -1,69 +0,0 @@ -# This list is generated from the output of: -# -# gcc -Q --help=optimizers -O0 -# -# with GCC 4.8.4 (Ubuntu 4.8.4-2ubuntu1-14.04.3). Yes, every one of these flags -# is on even with -O0 specified, and nothing changes when you add debugging -# options (-g/-g3/-gdwarf-4/etc.) in there. This should be updated every time -# the version of GCC used to compile changes. -# -# If you add an option here, it is your responsibility to comment it, with the -# following convention (feel free to add your own if there's not one suitable). -# DO YOUR RESEARCH. -# -# CBWITPOB: Can be wrong in the presence of bugs. When are you usually -# debugging? When there's a bug. Optimizations that can be wrong -# in the presence of bugs mean that, for example, you won't see -# a variable be modified when it actually happens--if it's -# modified due to the bug, as far as the debugger is concerned, -# it wasn't modified by the program, and things like conditional -# breakpoints won't work right, unless maybe it's a volatile -# variable. -# Inlining: Although GDB claims to track this correctly with -g3 and inject -# the code while you're stepping, it does not. You'll either be -# missing stack frames, or unable to view locals when you step -# to that frame--even if those locals exist nowhere else (i.e. -# not a function argument or tail return value). -# Eliding: Behavior may not change, but who knows where the values come -# from. -# Hoisting: Your program is not running instructions in the order of the -# code. Again, GDB claims to handle this, but it does not, or at -# least not well. -# Vectorizing: Great optimization, but the simulation of going through for -# loops is far from perfect, especially when you're dealing -# with bugs. -# -# And yes, these optimizations severely effect the quality of the debugging -# experience. Without these, you're lucky to be able to step into 80% of the -# stack, and of that 80%, you'll see anywhere from 50% to 100% of locals -# missing values. With these, I've never seen a stack frame I couldn't step -# into, and never seen when I look at a local. -# -set (REALLY_NO_OPTIMIZATION_FLAGS "-fno-short-enums" )# Binary-incompatible with code compiled otherwise. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-aggressive-loop-optimizations" ) # Changes behavior on overflow. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-branch-count-reg" )# Changes CPU instructions used. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-dce )# Can be wrong in the presence of bugs (CBWITPOB). set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-delete-null-pointer-checks )# CBWITPOB. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-dse )# CBWITPOB. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-early-inlining )# NO INLINING! Because... set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-gcse-lm )# Changes CPU instructions used. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-inline )# ...inlining also does things like elide locals. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-ira-hoist-pressure )# Might be irrelevant, but NO HOISTING! set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-ivopts )# Elides and changes instructions. CBWITPOB. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-jump-tables )# Changes CPU instructions for switch statements. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-move-loop-invariants )# NO HOISTING! set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-peephole )# Exploiting CPU quirks. CBWITPOB. set (REALLY_NO_OPTIMIZATION_FLAGS ="${REALLY_NO_OPTIMIZATION_FLAGS}+= -fno-prefetch-loop-arrays )# Changes CPU instructions, even GCC manual is ambivalent. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-rename-registers" )# Maybe wrong in the presence of bugs? -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-toplevel-reorder" )# Elides unused static variable, reorders globals. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-coalesce-vars" )# Elides temporaries. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-cselim" )# Reorders, violates C++ mem model, CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-forwprop" )# Reorders and changes instructions. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-loop-if-convert" )# Reorders and changes instructions. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-loop-im" )# Reorders and changes instructions. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-loop-optimize" )# Reorders and changes instructions. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-phiprop" )# NO HOISTING! Reorders and changes. CBWITPOB. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-pta" )# Less analysis means maybe less interference. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-reassoc" )# Elides and vectories. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-scev-cprop" )# Elides and changes instructions. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-vect-loop-version" )# E&C. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-web" )# E&C. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fno-tree-slp-vectorize" )# E&C. -set (REALLY_NO_OPTIMIZATION_FLAGS "${REALLY_NO_OPTIMIZATION_FLAGS} -fthreadsafe-statics" )# Slightly smaller in code that doesn't need to be TS. - -if (DEFINED CONAN_BUILD_COVERAGE) - if (${CONAN_BUILD_COVERAGE}) - include (cmake/CodeCoverage.cmake) - APPEND_COVERAGE_COMPILER_FLAGS() - SETUP_TARGET_FOR_COVERAGE_GCOVR_XML(NAME coverage EXECUTABLE ctest DEPENDENCIES ) - endif() -endif() -set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${REALLY_NO_OPTIMIZATION_FLAGS}") diff --git a/cmake/settings_gen.cmake b/cmake/settings_gen.cmake index 8db3cc24..acac0a62 100644 --- a/cmake/settings_gen.cmake +++ b/cmake/settings_gen.cmake @@ -58,7 +58,7 @@ macro(settings_gen_cpp flatbuffer_bin_path gen_out_path _target) add_custom_command( OUTPUT ${_GEN_HEADERS} COMMAND ${flatbuffer_bin_path} - ARGS -c -s -o ${gen_out_path} + ARGS -c -o ${gen_out_path} --no-prefix --scoped-enums --gen-mutable diff --git a/conanfile.py b/conanfile.py index e9a424af..17d83487 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,14 +1,16 @@ -from os.path import join from conan import ConanFile -from conan.tools.files import copy +from conan.errors import ConanInvalidConfiguration from conan.tools.build import check_min_cppstd -from conans import CMake +from conan.tools.cmake import CMakeToolchain, CMakeDeps, CMake, cmake_layout +from conan.tools.files import copy +from os.path import join -required_conan_version = ">=1.50.0" +required_conan_version = ">=1.60.0" class SISLConan(ConanFile): name = "sisl" - version = "8.0.2" + version = "11.1.3" + homepage = "https://github.com/eBay/sisl" description = "Library for fast data structures, utilities" topics = ("ebay", "components", "core", "efficiency") @@ -20,99 +22,169 @@ class SISLConan(ConanFile): options = { "shared": ['True', 'False'], "fPIC": ['True', 'False'], - 'malloc_impl' : ['libc', 'jemalloc'], - 'with_evhtp' : ['True', 'False'], + "coverage": ['True', 'False'], + "sanitize": ['True', 'False'], + 'prerelease' : ['True', 'False'], + 'malloc_impl' : ['libc', 'tcmalloc', 'jemalloc'], } default_options = { 'shared': False, 'fPIC': True, + 'coverage': False, + 'sanitize': False, + 'prerelease': False, 'malloc_impl': 'libc', - 'with_evhtp': False, } - generators = "cmake", "cmake_find_package" - exports_sources = ("CMakeLists.txt", "cmake/*", "src/*", "LICENSE") - - def build_requirements(self): - self.build_requires("benchmark/1.6.1") - self.build_requires("gtest/1.11.0") - - - def requirements(self): - # Custom packages + exports_sources = ( + "LICENSE", + "CMakeLists.txt", + "cmake/*", + "include/*", + "src/*", + ) - # Generic packages (conan-center) - self.requires("boost/1.79.0") - self.requires("cpr/1.8.1") - self.requires("cxxopts/2.2.1") - self.requires("flatbuffers/1.12.0") - self.requires("folly/2022.01.31.00") - self.requires("jwt-cpp/0.4.0") - self.requires("nlohmann_json/3.10.5") - self.requires("zmarok-semver/1.1.0") - self.requires("spdlog/1.10.0") - self.requires("userspace-rcu/0.11.4") - self.requires("prometheus-cpp/1.0.1") - self.requires("fmt/8.1.1", override=True) - self.requires("libevent/2.1.12", override=True) - self.requires("openssl/1.1.1q", override=True) - self.requires("xz_utils/5.2.5", override=True) - self.requires("zlib/1.2.12", override=True) - if self.options.malloc_impl == "jemalloc": - self.requires("jemalloc/5.2.1") - if self.options.with_evhtp: - self.requires("evhtp/1.2.18.2") + def _min_cppstd(self): + return 20 def validate(self): - if self.info.settings.compiler.cppstd: - check_min_cppstd(self, 20) + if self.settings.compiler.get_safe("cppstd"): + check_min_cppstd(self, self._min_cppstd()) def configure(self): + if self.settings.compiler in ["gcc"]: + self.options['pistache'].with_ssl: True if self.options.shared: - del self.options.fPIC + self.options.rm_safe("fPIC") + if self.settings.build_type == "Debug": + self.options.rm_safe("prerelease") + if self.options.coverage and self.options.sanitize: + raise ConanInvalidConfiguration("Sanitizer does not work with Code Coverage!") + if self.conf.get("tools.build:skip_test", default=False): + if self.options.coverage or self.options.sanitize: + raise ConanInvalidConfiguration("Coverage/Sanitizer requires Testing!") - def build(self): - cmake = CMake(self) + def build_requirements(self): + self.test_requires("benchmark/1.8.2") + self.test_requires("gtest/1.14.0") + + def requirements(self): + # Memory allocation + if self.options.malloc_impl == "tcmalloc": + self.requires("gperftools/2.15", transitive_headers=True) + elif self.options.malloc_impl == "jemalloc": + self.requires("jemalloc/5.3.0", transitive_headers=True) - definitions = {'CMAKE_EXPORT_COMPILE_COMMANDS': 'ON', - 'MEMORY_SANITIZER_ON': 'OFF', - 'EVHTP_ON': 'OFF', - 'MALLOC_IMPL': self.options.malloc_impl} - test_target = None + # Linux Specific Support + if self.settings.os in ["Linux"]: + self.requires("folly/nu2.2023.12.18.00", transitive_headers=True) + self.requires("userspace-rcu/nu2.0.14.0", transitive_headers=True) - if self.options.with_evhtp: - definitions['EVHTP_ON'] = 'ON' + # Generic packages (conan-center) + self.requires("boost/1.83.0", transitive_headers=True) + self.requires("cxxopts/3.1.1", transitive_headers=True) + self.requires("flatbuffers/23.5.26", transitive_headers=True) + self.requires("grpc/1.54.3", transitive_headers=True) + self.requires("nlohmann_json/3.11.2", transitive_headers=True) + self.requires("prometheus-cpp/1.1.0", transitive_headers=True) + self.requires("spdlog/1.12.0", transitive_headers=True) + self.requires("zmarok-semver/1.1.0", transitive_headers=True) + + if self.settings.os in ["Linux"]: + self.requires("breakpad/cci.20210521") + self.requires("fmt/10.0.0", override=True) + self.requires("libcurl/8.4.0", override=True) + self.requires("xz_utils/5.4.5", override=True) + + def layout(self): + cmake_layout(self) + + def generate(self): + # This generates "conan_toolchain.cmake" in self.generators_folder + tc = CMakeToolchain(self) + tc.variables["CONAN_CMAKE_SILENT_OUTPUT"] = "ON" + tc.variables["CTEST_OUTPUT_ON_FAILURE"] = "ON" + tc.variables["MEMORY_SANITIZER_ON"] = "OFF" + tc.variables["BUILD_COVERAGE"] = "OFF" + tc.variables['MALLOC_IMPL'] = self.options.malloc_impl + tc.variables["PACKAGE_VERSION"] = self.version + if self.options.get_safe("prerelease"): + tc.preprocessor_definitions["_PRERELEASE"] = "1" + if self.settings.build_type == "Debug": + tc.preprocessor_definitions["_PRERELEASE"] = "1" + if self.options.get_safe("coverage"): + tc.variables['BUILD_COVERAGE'] = 'ON' + elif self.options.get_safe("sanitize"): + tc.variables['MEMORY_SANITIZER_ON'] = 'ON' + tc.generate() + + # This generates "boost-config.cmake" and "grpc-config.cmake" etc in self.generators_folder + deps = CMakeDeps(self) + deps.generate() - cmake.configure(defs=definitions) + def build(self): + cmake = CMake(self) + cmake.configure() cmake.build() - cmake.test(target=test_target) + if not self.conf.get("tools.build:skip_test", default=False): + cmake.test() def package(self): lib_dir = join(self.package_folder, "lib") - copy(self, "LICENSE", self.source_folder, join(self.package_folder, "licenses/"), keep_path=False) + copy(self, "LICENSE", self.source_folder, join(self.package_folder, "licenses"), keep_path=False) copy(self, "*.lib", self.build_folder, lib_dir, keep_path=False) copy(self, "*.a", self.build_folder, lib_dir, keep_path=False) copy(self, "*.so*", self.build_folder, lib_dir, keep_path=False) copy(self, "*.dylib*", self.build_folder, lib_dir, keep_path=False) copy(self, "*.dll*", self.build_folder, join(self.package_folder, "bin"), keep_path=False) copy(self, "*.so*", self.build_folder, lib_dir, keep_path=False) + copy(self, "*.proto", join(self.source_folder, "src", "flip", "proto"), join(self.package_folder, "proto", "flip"), keep_path=False) + copy(self, "*", join(self.source_folder, "src", "flip", "client", "python"), join(self.package_folder, "bindings", "flip", "python"), keep_path=False) + copy(self, "*.py", join(self.build_folder, "src", "flip", "proto"), join(self.package_folder, "bindings", "flip", "python"), keep_path=False) - hdr_dir = join(self.package_folder, join("include", "sisl")) + copy(self, "*.h*", join(self.source_folder, "include"), join(self.package_folder, "include"), keep_path=True) - copy(self, "*.hpp", join(self.source_folder, "src"), hdr_dir, keep_path=True) - copy(self, "*.h", join(self.source_folder, "src"), hdr_dir, keep_path=True) + gen_dir = join(self.package_folder, "include", "sisl") + copy(self, "*.pb.h", join(self.build_folder, "src"), gen_dir, keep_path=True) + copy(self, "*security_config_generated.h", join(self.build_folder, "src"), gen_dir, keep_path=True) copy(self, "settings_gen.cmake", join(self.source_folder, "cmake"), join(self.package_folder, "cmake"), keep_path=False) + def _add_component(self, lib): + self.cpp_info.components[lib].libs = [lib] + self.cpp_info.components[lib].set_property("pkg_config_name", f"lib{lib}") + def package_info(self): - self.cpp_info.libs = ["sisl"] - self.cpp_info.cppflags.extend(["-Wno-unused-local-typedefs", "-fconcepts"]) - - if self.settings.os == "Linux": - self.cpp_info.cppflags.append("-D_POSIX_C_SOURCE=200809L") - self.cpp_info.cppflags.append("-D_FILE_OFFSET_BITS=64") - self.cpp_info.cppflags.append("-D_LARGEFILE64") - self.cpp_info.system_libs.extend(["dl", "pthread"]) - self.cpp_info.exelinkflags.extend(["-export-dynamic"]) - - if self.options.malloc_impl == 'jemalloc': - self.cpp_info.cppflags.append("-DUSE_JEMALLOC=1") + self._add_component("sisl") + self._add_component("flip") + + for component in self.cpp_info.components.values(): + component.requires.extend([ + "boost::boost", + "breakpad::breakpad", + "cxxopts::cxxopts", + "folly::folly", + "flatbuffers::flatbuffers", + "spdlog::spdlog", + "grpc::grpc", + "nlohmann_json::nlohmann_json", + "prometheus-cpp::prometheus-cpp", + "userspace-rcu::userspace-rcu", + "zmarok-semver::zmarok-semver", + ]) + if self.settings.os in ["Linux", "FreeBSD"]: + component.defines.append("_POSIX_C_SOURCE=200809L") + component.defines.append("_FILE_OFFSET_BITS=64") + component.defines.append("_LARGEFILE64") + component.system_libs.extend(["dl", "pthread"]) + component.exelinkflags.extend(["-export-dynamic"]) + if self.options.get_safe("prerelease"): + component.defines.append("_PRERELEASE=1") + if self.options.get_safe("sanitize"): + component.sharedlinkflags.append("-fsanitize=address") + component.exelinkflags.append("-fsanitize=address") + component.sharedlinkflags.append("-fsanitize=undefined") + component.exelinkflags.append("-fsanitize=undefined") + if self.options.malloc_impl == 'jemalloc': + self.cpp_info.defines.append("USE_JEMALLOC=1") + elif self.options.malloc_impl == 'tcmalloc': + self.cpp_info.defines.append("USING_TCMALLOC=1") diff --git a/include/sisl/auth_manager/token_client.hpp b/include/sisl/auth_manager/token_client.hpp new file mode 100644 index 00000000..427e45f5 --- /dev/null +++ b/include/sisl/auth_manager/token_client.hpp @@ -0,0 +1,27 @@ +#pragma once +#include + +namespace sisl { + +// Interface to get a token for authorization + +class TokenClient { +public: + virtual ~TokenClient() = default; + + virtual std::string get_token() = 0; +}; + +// the key value pairs (m_auth_header_key, get_token()) are sent as metadata in the grpc client context + +class GrpcTokenClient : public TokenClient { +public: + explicit GrpcTokenClient(std::string const& auth_header_key) : m_auth_header_key(auth_header_key) {} + virtual ~GrpcTokenClient() = default; + + std::string get_auth_header_key() const { return m_auth_header_key; } + +private: + std::string m_auth_header_key; +}; +} // namespace sisl \ No newline at end of file diff --git a/include/sisl/auth_manager/token_verifier.hpp b/include/sisl/auth_manager/token_verifier.hpp new file mode 100644 index 00000000..82ee0a68 --- /dev/null +++ b/include/sisl/auth_manager/token_verifier.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include + +namespace grpc { +class Status; +class ServerContext; +} // namespace grpc + +namespace sisl { + +// An interface for verifing a token for authorization. This can be used in conjunction with the TokenClient which is an +// interface to get a token. The implementation is deployment specific, one example is jwt based tokens provided by +// ebay/TrustFabric + +ENUM(VerifyCode, uint8_t, OK, UNAUTH, FORBIDDEN) + +// This class represents the return value to the token verify call. +// Derive from this class if the return value needs to contain some information from the decoded token. +class TokenVerifyState { +public: + TokenVerifyState() = default; + TokenVerifyState(VerifyCode const c, std::string const& m) : code(c), msg(m) {} + virtual ~TokenVerifyState() {} + VerifyCode code; + std::string msg; +}; + +using token_state_ptr = std::shared_ptr< TokenVerifyState >; + +class TokenVerifier { +public: + virtual ~TokenVerifier() = default; + virtual token_state_ptr verify(std::string const& token) const = 0; +}; + +// extracts the key value pairs (m_auth_header_key, get_token()) from grpc client context and verifies the token +class GrpcTokenVerifier : public TokenVerifier { +public: + explicit GrpcTokenVerifier(std::string const& auth_header_key) : m_auth_header_key(auth_header_key) {} + virtual ~GrpcTokenVerifier() = default; + + virtual grpc::Status verify(grpc::ServerContext const* srv_ctx) const = 0; + +protected: + std::string m_auth_header_key; +}; + +} // namespace sisl diff --git a/src/cache/evictor.hpp b/include/sisl/cache/evictor.hpp similarity index 96% rename from src/cache/evictor.hpp rename to include/sisl/cache/evictor.hpp index 500fe492..da2fb1b8 100644 --- a/src/cache/evictor.hpp +++ b/include/sisl/cache/evictor.hpp @@ -19,8 +19,9 @@ #include #include #include -#include "logging/logging.h" -#include "hash_entry_base.hpp" +#include +#include +#include namespace sisl { typedef ValueEntryBase CacheRecord; diff --git a/src/cache/hash_entry_base.hpp b/include/sisl/cache/hash_entry_base.hpp similarity index 100% rename from src/cache/hash_entry_base.hpp rename to include/sisl/cache/hash_entry_base.hpp diff --git a/src/cache/lru_evictor.hpp b/include/sisl/cache/lru_evictor.hpp similarity index 98% rename from src/cache/lru_evictor.hpp rename to include/sisl/cache/lru_evictor.hpp index ea461a48..c33f173f 100644 --- a/src/cache/lru_evictor.hpp +++ b/include/sisl/cache/lru_evictor.hpp @@ -21,8 +21,8 @@ #include #include #include -#include "fds/utils.hpp" -#include "evictor.hpp" +#include +#include using namespace boost::intrusive; diff --git a/src/cache/range_cache.hpp b/include/sisl/cache/range_cache.hpp similarity index 98% rename from src/cache/range_cache.hpp rename to include/sisl/cache/range_cache.hpp index f0ec7c06..cd2488db 100644 --- a/src/cache/range_cache.hpp +++ b/include/sisl/cache/range_cache.hpp @@ -17,8 +17,8 @@ #pragma once #include -#include "evictor.hpp" -#include "range_hashmap.hpp" +#include +#include namespace sisl { diff --git a/src/cache/range_hashmap.hpp b/include/sisl/cache/range_hashmap.hpp similarity index 99% rename from src/cache/range_hashmap.hpp rename to include/sisl/cache/range_hashmap.hpp index e919eae7..0c2c3d5b 100644 --- a/src/cache/range_hashmap.hpp +++ b/include/sisl/cache/range_hashmap.hpp @@ -31,10 +31,10 @@ #pragma GCC diagnostic pop #endif -#include "fds/buffer.hpp" -#include "hash_entry_base.hpp" -#include "fds/utils.hpp" -#include "utility/enum.hpp" +#include +#include +#include +#include namespace sisl { @@ -250,7 +250,7 @@ class MultiEntryHashNode : public boost::intrusive::slist_base_hook<> { K m_base_key; big_offset_t m_base_nth; - folly::small_vector< ValueEntryRange, 8, small_count_t > m_values; + folly::small_vector< ValueEntryRange, 8, folly::small_vector_policy::policy_size_type > m_values; public: MultiEntryHashNode(const K& base_key, big_offset_t nth) : m_base_key{base_key}, m_base_nth{nth} {} diff --git a/include/sisl/cache/simple_cache.hpp b/include/sisl/cache/simple_cache.hpp new file mode 100644 index 00000000..851a8378 --- /dev/null +++ b/include/sisl/cache/simple_cache.hpp @@ -0,0 +1,104 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include + +using namespace std::placeholders; + +namespace sisl { + +template < typename K, typename V > +class SimpleCache { +private: + std::shared_ptr< Evictor > m_evictor; + key_extractor_cb_t< K, V > m_key_extract_cb; + SimpleHashMap< K, V > m_map; + uint32_t m_record_family_id; + uint32_t m_per_value_size; + + static thread_local std::set< K > t_failed_keys; + +public: + SimpleCache(const std::shared_ptr< Evictor >& evictor, uint32_t num_buckets, uint32_t per_val_size, + key_extractor_cb_t< K, V >&& extract_cb, Evictor::can_evict_cb_t evict_cb = nullptr) : + m_evictor{evictor}, + m_key_extract_cb{std::move(extract_cb)}, + m_map{num_buckets, m_key_extract_cb, std::bind(&SimpleCache< K, V >::on_hash_operation, this, _1, _2, _3)}, + m_per_value_size{per_val_size} { + m_record_family_id = m_evictor->register_record_family(std::move(evict_cb)); + } + + ~SimpleCache() { m_evictor->unregister_record_family(m_record_family_id); } + + bool insert(const V& value) { + K k = m_key_extract_cb(value); + return m_map.insert(k, value); + } + + bool upsert(const V& value) { + K k = m_key_extract_cb(value); + return m_map.upsert(k, value); + } + + bool remove(const K& key, V& out_val) { return m_map.erase(key, out_val); } + + bool get(const K& key, V& out_val) { return m_map.get(key, out_val); } + +private: + void on_hash_operation(const CacheRecord& r, const K& key, const hash_op_t op) { + CacheRecord& record = const_cast< CacheRecord& >(r); + const auto hash_code = SimpleHashMap< K, V >::compute_hash(key); + + switch (op) { + case hash_op_t::CREATE: + record.set_record_family(m_record_family_id); + record.set_size(m_per_value_size); + if (!m_evictor->add_record(hash_code, record)) { + // We were not able to evict any, so mark this record and we will erase them upon all callbacks are done + t_failed_keys.insert(key); + } + break; + + case hash_op_t::DELETE: + if (t_failed_keys.size()) { + // Check if this is a delete of failed keys, if so lets not add it to record + if (t_failed_keys.find(key) != t_failed_keys.end()) { return; } + } + m_evictor->remove_record(hash_code, record); + break; + + case hash_op_t::ACCESS: + m_evictor->record_accessed(hash_code, record); + break; + + case hash_op_t::RESIZE: { + DEBUG_ASSERT(false, "Don't expect RESIZE operation for simple cache entries"); + break; + } + default: + DEBUG_ASSERT(false, "Invalid hash_op"); + break; + } + } +}; + +template < typename K, typename V > +thread_local std::set< K > SimpleCache< K, V >::t_failed_keys; +} // namespace sisl diff --git a/include/sisl/cache/simple_hashmap.hpp b/include/sisl/cache/simple_hashmap.hpp new file mode 100644 index 00000000..bd5b389f --- /dev/null +++ b/include/sisl/cache/simple_hashmap.hpp @@ -0,0 +1,360 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include +#include +#if defined __clang__ or defined __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#pragma GCC diagnostic ignored "-Wattributes" +#endif +#include +#if defined __clang__ or defined __GNUC__ +#pragma GCC diagnostic pop +#endif + +#include +#include +#include + +namespace sisl { + +template < typename K, typename V > +class SimpleHashBucket; + +ENUM(hash_op_t, uint8_t, CREATE, ACCESS, DELETE, RESIZE) + +template < typename K > +using key_access_cb_t = std::function< void(const ValueEntryBase&, const K&, const hash_op_t) >; + +template < typename K, typename V > +using key_extractor_cb_t = std::function< K(const V&) >; + +static constexpr size_t s_start_seed = 0; // TODO: Pickup a better seed + +///////////////////////////////////////////// RangeHashMap Declaration /////////////////////////////////// +template < typename K, typename V > +class SimpleHashMap { +private: + uint32_t m_nbuckets; + SimpleHashBucket< K, V >* m_buckets; + key_extractor_cb_t< K, V > m_key_extract_cb; + key_access_cb_t< K > m_key_access_cb; + + static thread_local SimpleHashMap< K, V >* s_cur_hash_map; + +#ifdef GLOBAL_HASHSET_LOCK + mutable std::mutex m; +#endif + +public: + SimpleHashMap(uint32_t nBuckets, const key_extractor_cb_t< K, V >& key_extractor, + key_access_cb_t< K > access_cb = nullptr); + ~SimpleHashMap(); + + bool insert(const K& key, const V& value); + bool upsert(const K& key, const V& value); + bool get(const K& input_key, V& out_val); + bool erase(const K& key, V& out_val); + bool update(const K& key, auto&& update_cb); + bool upsert_or_delete(const K& key, auto&& update_or_delete_cb); + + static void set_current_instance(SimpleHashMap< K, V >* hmap) { s_cur_hash_map = hmap; } + static SimpleHashMap< K, V >* get_current_instance() { return s_cur_hash_map; } + static key_access_cb_t< K >& get_access_cb() { return get_current_instance()->m_key_access_cb; } + static key_extractor_cb_t< K, V >& extractor_cb() { return get_current_instance()->m_key_extract_cb; } + + template < typename... Args > + static void call_access_cb(Args&&... args) { + if (get_current_instance()->m_key_access_cb) { + (get_current_instance()->m_key_access_cb)(std::forward< Args >(args)...); + } + } + static size_t compute_hash(const K& key) { + size_t seed = s_start_seed; + boost::hash_combine(seed, key); + return seed; + } + +private: + SimpleHashBucket< K, V >& get_bucket(const K& key) const; + SimpleHashBucket< K, V >& get_bucket(size_t hash_code) const; +}; + +///////////////////////////////////////////// MultiEntryHashNode Definitions /////////////////////////////////// +template < typename V > +struct SingleEntryHashNode : public ValueEntryBase, public boost::intrusive::slist_base_hook<> { + V m_value; + SingleEntryHashNode(const V& value) : m_value{value} {} +}; + +///////////////////////////////////////////// ValueEntryRange Definitions /////////////////////////////////// + +template < typename K, typename V > +thread_local sisl::SimpleHashMap< K, V >* sisl::SimpleHashMap< K, V >::s_cur_hash_map{nullptr}; + +///////////////////////////////////////////// SimpleHashBucket Definitions /////////////////////////////////// +template < typename K, typename V > +class SimpleHashBucket { +private: +#ifndef GLOBAL_HASHSET_LOCK + mutable folly::SharedMutexWritePriority m_lock; +#endif + typedef boost::intrusive::slist< SingleEntryHashNode< V > > hash_node_list_t; + hash_node_list_t m_list; + +public: + SimpleHashBucket() = default; + + ~SimpleHashBucket() { + auto it{m_list.begin()}; + while (it != m_list.end()) { + SingleEntryHashNode< V >* n = &*it; + it = m_list.erase(it); + delete n; + } + } + + bool insert(const K& input_key, const V& input_value, bool overwrite_ok) { +#ifndef GLOBAL_HASHSET_LOCK + folly::SharedMutexWritePriority::WriteHolder holder(m_lock); +#endif + SingleEntryHashNode< V >* n = nullptr; + auto it = m_list.begin(); + for (auto itend{m_list.end()}; it != itend; ++it) { + const K k = SimpleHashMap< K, V >::extractor_cb()(it->m_value); + if (input_key > k) { + break; + } else if (input_key == k) { + n = &*it; + } + } + + if (n == nullptr) { + n = new SingleEntryHashNode< V >(input_value); + m_list.insert(it, *n); + access_cb(*n, input_key, hash_op_t::CREATE); + return true; + } else { + if (overwrite_ok) { + n->m_value = input_value; + access_cb(*n, input_key, hash_op_t::ACCESS); + } + return false; + } + } + + bool get(const K& input_key, V& out_val) { +#ifndef GLOBAL_HASHSET_LOCK + folly::SharedMutexWritePriority::ReadHolder holder(m_lock); +#endif + bool found{false}; + for (const auto& n : m_list) { + const K k = SimpleHashMap< K, V >::extractor_cb()(n.m_value); + if (input_key > k) { + break; + } else if (input_key == k) { + out_val = n.m_value; + found = true; + access_cb(n, input_key, hash_op_t::ACCESS); + break; + } + } + return found; + } + + bool erase(const K& input_key, V& out_val) { +#ifndef GLOBAL_HASHSET_LOCK + folly::SharedMutexWritePriority::WriteHolder holder(m_lock); +#endif + SingleEntryHashNode< V >* n = nullptr; + + auto it = m_list.begin(); + for (auto itend{m_list.end()}; it != itend; ++it) { + const K k = SimpleHashMap< K, V >::extractor_cb()(it->m_value); + if (input_key > k) { + break; + } else if (input_key == k) { + n = &*it; + break; + } + } + + if (n) { + access_cb(*n, input_key, hash_op_t::DELETE); + out_val = n->m_value; + m_list.erase(it); + delete n; + return true; + } + return false; + } + + bool upsert_or_delete(const K& input_key, auto&& update_or_delete_cb) { +#ifndef GLOBAL_HASHSET_LOCK + folly::SharedMutexWritePriority::WriteHolder holder(m_lock); +#endif + SingleEntryHashNode< V >* n = nullptr; + + auto it = m_list.begin(); + for (auto itend{m_list.end()}; it != itend; ++it) { + const K k = SimpleHashMap< K, V >::extractor_cb()(it->m_value); + if (input_key > k) { + break; + } else if (input_key == k) { + n = &*it; + break; + } + } + + bool found{true}; + if (n == nullptr) { + n = new SingleEntryHashNode< V >(V{}); + m_list.insert(it, *n); + access_cb(*n, input_key, hash_op_t::CREATE); + found = false; + } + + if (update_or_delete_cb(n->m_value, found)) { + access_cb(*n, input_key, hash_op_t::DELETE); + m_list.erase(it); + delete n; + } else { + access_cb(*n, input_key, hash_op_t::ACCESS); + } + + return !found; + } + + bool update(const K& input_key, auto&& update_cb) { +#ifndef GLOBAL_HASHSET_LOCK + folly::SharedMutexWritePriority::ReadHolder holder(m_lock); +#endif + bool found{false}; + for (auto& n : m_list) { + const K k = SimpleHashMap< K, V >::extractor_cb()(n.m_value); + if (input_key > k) { + break; + } else if (input_key == k) { + found = true; + access_cb(n, input_key, hash_op_t::ACCESS); + update_cb(n.m_value); + break; + } + } + return found; + } + +private: + static void access_cb(const SingleEntryHashNode< V >& node, const K& key, hash_op_t op) { + SimpleHashMap< K, V >::call_access_cb((const ValueEntryBase&)node, key, op); + } +}; + +///////////////////////////////////////////// RangeHashMap Definitions /////////////////////////////////// +template < typename K, typename V > +SimpleHashMap< K, V >::SimpleHashMap(uint32_t nBuckets, const key_extractor_cb_t< K, V >& extract_cb, + key_access_cb_t< K > access_cb) : + m_nbuckets{nBuckets}, m_key_extract_cb{extract_cb}, m_key_access_cb{std::move(access_cb)} { + m_buckets = new SimpleHashBucket< K, V >[nBuckets]; +} + +template < typename K, typename V > +SimpleHashMap< K, V >::~SimpleHashMap() { + delete[] m_buckets; +} + +template < typename K, typename V > +bool SimpleHashMap< K, V >::insert(const K& key, const V& value) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).insert(key, value, false /* overwrite_ok */); +} + +template < typename K, typename V > +bool SimpleHashMap< K, V >::upsert(const K& key, const V& value) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).insert(key, value, true /* overwrite_ok */); +} + +template < typename K, typename V > +bool SimpleHashMap< K, V >::get(const K& key, V& out_val) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).get(key, out_val); +} + +template < typename K, typename V > +bool SimpleHashMap< K, V >::erase(const K& key, V& out_val) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).erase(key, out_val); +} + +/// This is a special atomic operation where user can insert_or_update_or_erase based on condition atomically. It +/// performs differently based on certain conditions. +/// +/// NOTE: This method works only if the Value is default constructible +/// +/// * If the key does not exist, it will insert a default value and does the callback +/// +/// * Callback should so one of the following 2 operation +/// a) The current value can be updated and return false from callback - it works like an upsert operation +/// b) Return true from callback - in that case it will behave like erase operation of the KV +/// +/// Returns true if the value was inserted +template < typename K, typename V > +bool SimpleHashMap< K, V >::upsert_or_delete(const K& key, auto&& update_or_delete_cb) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).upsert_or_delete(key, std::move(update_or_delete_cb)); +} + +template < typename K, typename V > +bool SimpleHashMap< K, V >::update(const K& key, auto&& update_cb) { +#ifdef GLOBAL_HASHSET_LOCK + std::lock_guard< std::mutex > lk(m); +#endif + set_current_instance(this); + return get_bucket(key).update(key, std::move(update_cb)); +} + +template < typename K, typename V > +SimpleHashBucket< K, V >& SimpleHashMap< K, V >::get_bucket(const K& key) const { + return (m_buckets[compute_hash(key) % m_nbuckets]); +} + +template < typename K, typename V > +SimpleHashBucket< K, V >& SimpleHashMap< K, V >::get_bucket(size_t hash_code) const { + return (m_buckets[hash_code % m_nbuckets]); +} + +} // namespace sisl diff --git a/src/fds/atomic_status_counter.hpp b/include/sisl/fds/atomic_status_counter.hpp similarity index 100% rename from src/fds/atomic_status_counter.hpp rename to include/sisl/fds/atomic_status_counter.hpp diff --git a/src/fds/bitset.hpp b/include/sisl/fds/bitset.hpp similarity index 97% rename from src/fds/bitset.hpp rename to include/sisl/fds/bitset.hpp index 240afb7f..616fb0e2 100644 --- a/src/fds/bitset.hpp +++ b/include/sisl/fds/bitset.hpp @@ -38,7 +38,7 @@ #pragma GCC diagnostic pop #endif -#include "logging/logging.h" +#include #include "bitword.hpp" #include "buffer.hpp" @@ -208,7 +208,7 @@ class BitsetImpl { const uint64_t size{(alignment_size > 0) ? round_up(bitset_serialized::nbytes(nbits), alignment_size) : bitset_serialized::nbytes(nbits)}; m_buf = make_byte_array_with_deleter(static_cast< uint32_t >(size), alignment_size); - m_s = new (m_buf->bytes) bitset_serialized{m_id, nbits, 0, alignment_size}; + m_s = new (m_buf->bytes()) bitset_serialized{m_id, nbits, 0, alignment_size}; } // this makes a shared copy of the rhs so that modifications of the shared version @@ -224,18 +224,18 @@ class BitsetImpl { // NOTE: This assumes that the passed byte_array already has an initialized bitset_serialized structure // Also assume that the words byte array contains packed word_t data since packed Word data is illegal // with any class besides POD - assert(b->size >= sizeof(bitset_serialized)); + assert(b->size() >= sizeof(bitset_serialized)); // get the header info - const bitset_serialized* const ptr{reinterpret_cast< const bitset_serialized* >(b->bytes)}; + const bitset_serialized* const ptr{reinterpret_cast< const bitset_serialized* >(b->cbytes())}; assert(ptr->m_word_bits == bitword_type::bits()); const uint64_t nbits{ptr->m_nbits}; const uint64_t total_bytes{bitset_serialized::nbytes(nbits)}; const uint32_t alignment_size{opt_alignment_size ? (*opt_alignment_size) : ptr->m_alignment_size}; const uint64_t size{(alignment_size > 0) ? round_up(total_bytes, alignment_size) : total_bytes}; - assert(b->size >= total_bytes); + assert(b->size() >= total_bytes); m_buf = make_byte_array_with_deleter(static_cast< uint32_t >(size), alignment_size); - m_s = new (m_buf->bytes) bitset_serialized{ptr->m_id, nbits, ptr->m_skip_bits, alignment_size, false}; - const word_t* b_words{reinterpret_cast< const word_t* >(b->bytes + sizeof(bitset_serialized))}; + m_s = new (m_buf->bytes()) bitset_serialized{ptr->m_id, nbits, ptr->m_skip_bits, alignment_size, false}; + const word_t* b_words{reinterpret_cast< const word_t* >(b->cbytes() + sizeof(bitset_serialized))}; // copy the data std::uninitialized_copy(b_words, std::next(b_words, m_s->m_words_cap), m_s->get_words()); } @@ -248,7 +248,7 @@ class BitsetImpl { const uint64_t size{(alignment_size > 0) ? round_up(bitset_serialized::nbytes(nbits), alignment_size) : bitset_serialized::nbytes(nbits)}; m_buf = make_byte_array_with_deleter(static_cast< uint32_t >(size), alignment_size); - m_s = new (m_buf->bytes) bitset_serialized{id, nbits, 0, alignment_size, false}; + m_s = new (m_buf->bytes()) bitset_serialized{id, nbits, 0, alignment_size, false}; // copy the data into the uninitialized bitset std::uninitialized_copy(start_ptr, end_ptr, m_s->get_words()); @@ -264,7 +264,7 @@ class BitsetImpl { const uint64_t size{(alignment_size > 0) ? round_up(bitset_serialized::nbytes(nbits), alignment_size) : bitset_serialized::nbytes(nbits)}; m_buf = make_byte_array_with_deleter(static_cast< uint32_t >(size), alignment_size); - m_s = new (m_buf->bytes) bitset_serialized{id, nbits, 0, alignment_size, false}; + m_s = new (m_buf->bytes()) bitset_serialized{id, nbits, 0, alignment_size, false}; // copy the data into the unitialized bitset std::uninitialized_copy(start_itr, end_itr, m_s->get_words()); @@ -485,9 +485,9 @@ class BitsetImpl { { ReadLockGuard other_lock{&other}; // ensure distinct buffers - if ((m_buf->size != other.m_buf->size) || (m_buf == other.m_buf)) { - m_buf = make_byte_array_with_deleter(other.m_buf->size, other.m_s->m_alignment_size); - m_s = new (m_buf->bytes) + if ((m_buf->size() != other.m_buf->size()) || (m_buf == other.m_buf)) { + m_buf = make_byte_array_with_deleter(other.m_buf->size(), other.m_s->m_alignment_size); + m_s = new (m_buf->bytes()) bitset_serialized{other.m_s->m_id, other.m_s->m_nbits, other.m_s->m_skip_bits, other.m_s->m_alignment_size, false}; std::uninitialized_copy(other.m_s->get_words_const(), other.m_s->end_words_const(), @@ -496,7 +496,7 @@ class BitsetImpl { // Word array is initialized here so std::copy suffices for some or all const auto old_words_cap{m_s->m_words_cap}; if (other.m_s->m_words_cap > old_words_cap) { - m_s = new (m_buf->bytes) + m_s = new (m_buf->bytes()) bitset_serialized{other.m_s->m_id, other.m_s->m_nbits, other.m_s->m_skip_bits, other.m_s->m_alignment_size, false}; // copy into previously initialized spaces @@ -513,7 +513,7 @@ class BitsetImpl { std::next(m_s->get_words(), old_words_cap)); } - m_s = new (m_buf->bytes) + m_s = new (m_buf->bytes()) bitset_serialized{other.m_s->m_id, other.m_s->m_nbits, other.m_s->m_skip_bits, other.m_s->m_alignment_size, false}; std::copy(other.m_s->get_words_const(), other.m_s->end_words_const(), m_s->get_words()); @@ -536,11 +536,11 @@ class BitsetImpl { // ensure distinct buffers bool uninitialized{false}; const auto old_words_cap{m_s->m_words_cap}; - if ((m_buf->size != size) || (m_buf == other.m_buf)) { + if ((m_buf->size() != size) || (m_buf == other.m_buf)) { m_buf = make_byte_array_with_deleter(size, alignment_size); uninitialized = true; } - m_s = new (m_buf->bytes) bitset_serialized{other.m_s->m_id, nbits, 0, alignment_size, false}; + m_s = new (m_buf->bytes()) bitset_serialized{other.m_s->m_id, nbits, 0, alignment_size, false}; const auto new_words_cap{m_s->m_words_cap}; bitword_type* word_ptr{m_s->get_words()}; const uint8_t rhs_offset{other.get_word_offset(0)}; @@ -667,17 +667,17 @@ class BitsetImpl { delete ptr; } }}}; - word_t* word_ptr{reinterpret_cast< word_t* >(buf->bytes + sizeof(bitset_serialized))}; + word_t* word_ptr{reinterpret_cast< word_t* >(buf->bytes() + sizeof(bitset_serialized))}; if (std::is_standard_layout_v< bitword_type > && std::is_trivial_v< value_type > && (sizeof(value_type) == sizeof(bitword_type))) { const size_t num_words{static_cast< size_t >(m_s->end_words_const() - get_word_const(0))}; const uint64_t skip_bits{get_word_offset(0)}; - new (buf->bytes) bitset_serialized{m_s->m_id, num_bits + skip_bits, skip_bits, alignment_size, false}; + new (buf->bytes()) bitset_serialized{m_s->m_id, num_bits + skip_bits, skip_bits, alignment_size, false}; std::memcpy(static_cast< void* >(word_ptr), static_cast< const void* >(get_word_const(0)), num_words * sizeof(word_t)); } else { // non trivial, word by word copy the unshifted data words - new (buf->bytes) bitset_serialized{m_s->m_id, num_bits, 0, alignment_size, false}; + new (buf->bytes()) bitset_serialized{m_s->m_id, num_bits, 0, alignment_size, false}; uint64_t current_bit{0}; for (uint64_t word_num{0}; word_num < total_words; ++word_num, ++word_ptr, current_bit += word_size()) { new (word_ptr) word_t{get_word_value(current_bit)}; @@ -1146,7 +1146,7 @@ class BitsetImpl { const uint64_t new_nbits{nbits + new_skip_bits}; auto new_buf{make_byte_array_with_deleter(bitset_serialized::nbytes(new_nbits), m_s->m_alignment_size)}; - auto new_s{new (new_buf->bytes) + auto new_s{new (new_buf->bytes()) bitset_serialized{m_s->m_id, new_nbits, new_skip_bits, m_s->m_alignment_size, false}}; const auto new_cap{new_s->m_words_cap}; diff --git a/src/fds/bitword.hpp b/include/sisl/fds/bitword.hpp similarity index 98% rename from src/fds/bitword.hpp rename to include/sisl/fds/bitword.hpp index aea5d4d8..69c910eb 100644 --- a/src/fds/bitword.hpp +++ b/include/sisl/fds/bitword.hpp @@ -29,7 +29,7 @@ #include -#include "utility/enum.hpp" +#include namespace sisl { @@ -398,6 +398,16 @@ class Bitword { } } + bool get_prev_set_bit(uint8_t start, uint8_t* p_set_bit) const { + const word_t e{extract(0, start + 1)}; + if (e) { + *p_set_bit = logBase2(e); + return true; + } else { + return false; + } + } + uint8_t get_next_reset_bits(const uint8_t start, uint8_t* const pcount) const { assert(start < bits()); assert(pcount); @@ -536,10 +546,9 @@ class Bitword { std::string to_string() const { std::ostringstream oSS{}; - const word_t e{m_bits.get()}; - word_t mask{static_cast< word_t >(bit_mask[bits() - 1])}; - for (uint8_t bit{0}; bit < bits(); ++bit, mask >>= 1) { - oSS << (((e & mask) == mask) ? '1' : '0'); + const word_t e = m_bits.get(); + for (uint8_t bit{0}; bit < bits(); ++bit) { + oSS << (((e & bit_mask[bit]) == bit_mask[bit]) ? '1' : '0'); } return oSS.str(); } diff --git a/src/fds/buffer.hpp b/include/sisl/fds/buffer.hpp similarity index 54% rename from src/fds/buffer.hpp rename to include/sisl/fds/buffer.hpp index e112f1db..8f514d50 100644 --- a/src/fds/buffer.hpp +++ b/include/sisl/fds/buffer.hpp @@ -24,19 +24,108 @@ #include #ifdef __linux__ #include +#include +#endif +#include +#include +#include +#include "utils.hpp" + +#ifndef NDEBUG +#ifndef _DEBUG +#define _DEBUG +#endif #endif - -#include -#include -#include namespace sisl { struct blob { - uint8_t* bytes; - uint32_t size; +protected: + uint8_t* bytes_{nullptr}; + uint32_t size_{0}; +#ifdef _DEBUG + bool is_const_{false}; +#endif + +public: + blob() = default; + blob(uint8_t* b, uint32_t s) : bytes_{b}, size_{s} {} + blob(uint8_t const* b, uint32_t s) : bytes_{const_cast< uint8_t* >(b)}, size_{s} { +#ifdef _DEBUG + is_const_ = true; +#endif + } + + uint8_t* bytes() { + DEBUG_ASSERT_EQ(is_const_, false, "Trying to access writeable bytes with const declaration"); + return bytes_; + } + uint32_t size() const { return size_; } + uint8_t const* cbytes() const { return bytes_; } + + void set_bytes(uint8_t* b) { + DEBUG_ASSERT_EQ(is_const_, false, "Trying to access writeable bytes with const declaration"); + bytes_ = b; + } + + void set_bytes(uint8_t const* b) { +#ifdef _DEBUG + is_const_ = false; +#endif + bytes_ = const_cast< uint8_t* >(b); + } + void set_size(uint32_t s) { size_ = s; } +}; + +using sg_iovs_t = folly::small_vector< iovec, 4 >; +struct sg_list { + uint64_t size; // total size of data pointed by iovs; + sg_iovs_t iovs; +}; + +struct sg_iterator { + sg_iterator(const sg_iovs_t& v) : m_input_iovs{v} { assert(v.size() > 0); } + + sg_iovs_t next_iovs(uint32_t size) { + sg_iovs_t ret_iovs; + auto remain_size = size; + + while ((remain_size > 0) && (m_cur_index < m_input_iovs.size())) { + const auto& inp_iov = m_input_iovs[m_cur_index]; + iovec this_iov; + this_iov.iov_base = static_cast< uint8_t* >(inp_iov.iov_base) + m_cur_offset; + if (remain_size < inp_iov.iov_len - m_cur_offset) { + this_iov.iov_len = remain_size; + m_cur_offset += remain_size; + } else { + this_iov.iov_len = inp_iov.iov_len - m_cur_offset; + ++m_cur_index; + m_cur_offset = 0; + } + + ret_iovs.push_back(this_iov); + assert(remain_size >= this_iov.iov_len); + remain_size -= this_iov.iov_len; + } + + return ret_iovs; + } + + void move_offset(const uint32_t size) { + auto remain_size = size; + const auto input_iovs_size = m_input_iovs.size(); + for (; (remain_size > 0) && (m_cur_index < input_iovs_size); ++m_cur_index, m_cur_offset = 0) { + const auto& inp_iov = m_input_iovs[m_cur_index]; + if (remain_size < inp_iov.iov_len - m_cur_offset) { + m_cur_offset += remain_size; + return; + } + remain_size -= inp_iov.iov_len - m_cur_offset; + } + } - blob() : blob{nullptr, 0} {} - blob(uint8_t* const b, const uint32_t s) : bytes{b}, size{s} {} + const sg_iovs_t& m_input_iovs; + uint64_t m_cur_offset{0}; + size_t m_cur_index{0}; }; // typedef size_t buftag_t; @@ -104,9 +193,7 @@ class AlignedAllocatorImpl { virtual uint8_t* aligned_pool_alloc(const size_t align, const size_t sz, const sisl::buftag tag) { return aligned_alloc(align, sz, tag); }; - virtual void aligned_pool_free(uint8_t* const b, const size_t sz, const sisl::buftag tag) { - aligned_free(b, tag); - }; + virtual void aligned_pool_free(uint8_t* const b, const size_t, const sisl::buftag tag) { aligned_free(b, tag); }; virtual size_t buf_size(uint8_t* buf) const { #ifdef __linux__ @@ -186,92 +273,131 @@ class aligned_shared_ptr : public std::shared_ptr< T > { aligned_shared_ptr(T* p) : std::shared_ptr< T >(p) {} }; +struct io_blob; +using io_blob_list_t = folly::small_vector< sisl::io_blob, 4 >; + struct io_blob : public blob { - bool aligned{false}; +protected: + bool aligned_{false}; +public: io_blob() = default; - io_blob(const size_t sz, const uint32_t align_size = 512, const buftag tag = buftag::common) { - buf_alloc(sz, align_size, tag); - } - io_blob(uint8_t* const bytes, const uint32_t size, const bool is_aligned) : - blob(bytes, size), aligned{is_aligned} {} + io_blob(size_t sz, uint32_t align_size = 512, buftag tag = buftag::common) { buf_alloc(sz, align_size, tag); } + io_blob(uint8_t* bytes, uint32_t size, bool is_aligned) : blob(bytes, size), aligned_{is_aligned} {} + io_blob(uint8_t const* bytes, uint32_t size, bool is_aligned) : blob(bytes, size), aligned_{is_aligned} {} ~io_blob() = default; - void buf_alloc(const size_t sz, const uint32_t align_size = 512, const buftag tag = buftag::common) { - aligned = (align_size != 0); - blob::size = sz; - blob::bytes = aligned ? sisl_aligned_alloc(align_size, sz, tag) : (uint8_t*)malloc(sz); + void buf_alloc(size_t sz, uint32_t align_size = 512, buftag tag = buftag::common) { + aligned_ = (align_size != 0); + blob::size_ = sz; + blob::bytes_ = aligned_ ? sisl_aligned_alloc(align_size, sz, tag) : (uint8_t*)malloc(sz); } - void buf_free(const buftag tag = buftag::common) const { - aligned ? sisl_aligned_free(blob::bytes, tag) : std::free(blob::bytes); + void buf_free(buftag tag = buftag::common) const { + aligned_ ? sisl_aligned_free(blob::bytes_, tag) : std::free(blob::bytes_); } - void buf_realloc(const size_t new_size, const uint32_t align_size = 512, const buftag tag = buftag::common) { + void buf_realloc(size_t new_size, uint32_t align_size = 512, [[maybe_unused]] buftag tag = buftag::common) { uint8_t* new_buf{nullptr}; - if (aligned) { + if (aligned_) { // aligned before, so do not need check for new align size, once aligned will be aligned on realloc also - new_buf = sisl_aligned_realloc(blob::bytes, align_size, new_size, blob::size); + new_buf = sisl_aligned_realloc(blob::bytes_, align_size, new_size, blob::size_); } else if (align_size != 0) { // Not aligned before, but need aligned now uint8_t* const new_buf{sisl_aligned_alloc(align_size, new_size, buftag::common)}; - std::memcpy(static_cast< void* >(new_buf), static_cast< const void* >(blob::bytes), - std::min(new_size, static_cast< size_t >(blob::size))); - std::free(blob::bytes); + std::memcpy(static_cast< void* >(new_buf), static_cast< const void* >(blob::bytes_), + std::min(new_size, static_cast< size_t >(blob::size_))); + std::free(blob::bytes_); } else { // don't bother about alignment, just do standard realloc - new_buf = (uint8_t*)std::realloc(blob::bytes, new_size); + new_buf = (uint8_t*)std::realloc(blob::bytes_, new_size); } - blob::size = new_size; - blob::bytes = new_buf; + blob::size_ = new_size; + blob::bytes_ = new_buf; + } + + bool is_aligned() const { return aligned_; } + + static io_blob from_string(const std::string& s) { + return io_blob{r_cast< const uint8_t* >(s.data()), uint32_cast(s.size()), false}; + } + + static io_blob_list_t sg_list_to_ioblob_list(const sg_list& sglist) { + io_blob_list_t ret_list; + for (const auto& iov : sglist.iovs) { + ret_list.emplace_back(r_cast< uint8_t* >(const_cast< void* >(iov.iov_base)), uint32_cast(iov.iov_len), + false); + } + return ret_list; } }; /* An extension to blob where the buffer it holds is allocated by constructor and freed during destruction. The only * reason why we have this instead of using vector< uint8_t > is that this supports allocating in aligned memory */ -struct byte_array_impl : public io_blob { - byte_array_impl(const uint32_t sz, const uint32_t alignment = 0, const buftag tag = buftag::common) : +struct io_blob_safe final : public io_blob { +public: + buftag m_tag; + +public: + io_blob_safe() = default; + io_blob_safe(uint32_t sz, uint32_t alignment = 0, buftag tag = buftag::common) : io_blob(sz, alignment, tag), m_tag{tag} {} - byte_array_impl(uint8_t* const bytes, const uint32_t size, const bool is_aligned) : - io_blob(bytes, size, is_aligned) {} - ~byte_array_impl() { io_blob::buf_free(m_tag); } + io_blob_safe(uint8_t* bytes, uint32_t size, bool is_aligned) : io_blob(bytes, size, is_aligned) {} + io_blob_safe(uint8_t const* bytes, uint32_t size, bool is_aligned) : io_blob(bytes, size, is_aligned) {} + ~io_blob_safe() { + if (blob::bytes_ != nullptr) { io_blob::buf_free(m_tag); } + } - buftag m_tag; + io_blob_safe(io_blob_safe const& other) = delete; + io_blob_safe(io_blob_safe&& other) : io_blob(std::move(other)), m_tag(other.m_tag) { + other.bytes_ = nullptr; + other.size_ = 0; + } + + io_blob_safe& operator=(io_blob_safe const& other) = delete; // Delete copy constructor + io_blob_safe& operator=(io_blob_safe&& other) { + if (blob::bytes_ != nullptr) { this->buf_free(m_tag); } + + *((io_blob*)this) = std::move(*((io_blob*)&other)); + m_tag = other.m_tag; + + other.bytes_ = nullptr; + other.size_ = 0; + return *this; + } }; -using byte_array = std::shared_ptr< byte_array_impl >; -inline byte_array make_byte_array(const uint32_t sz, const uint32_t alignment = 0, const buftag tag = buftag::common) { - return std::make_shared< byte_array_impl >(sz, alignment, tag); -} +using byte_array_impl = io_blob_safe; -inline byte_array to_byte_array(const sisl::io_blob& blob) { - return std::make_shared< byte_array_impl >(blob.bytes, blob.size, blob.aligned); +using byte_array = std::shared_ptr< io_blob_safe >; +inline byte_array make_byte_array(uint32_t sz, uint32_t alignment = 0, buftag tag = buftag::common) { + return std::make_shared< io_blob_safe >(sz, alignment, tag); } struct byte_view { public: byte_view() = default; - byte_view(const uint32_t sz, const uint32_t alignment = 0, const buftag tag = buftag::common) { + byte_view(uint32_t sz, uint32_t alignment = 0, buftag tag = buftag::common) { m_base_buf = make_byte_array(sz, alignment, tag); - m_view = *m_base_buf; + m_view.set_bytes(m_base_buf->cbytes()); + m_view.set_size(m_base_buf->size()); } - byte_view(byte_array buf) : byte_view(std::move(buf), 0, buf->size) {} - byte_view(byte_array buf, const uint32_t offset, const uint32_t sz) { + byte_view(byte_array buf) : byte_view(std::move(buf), 0u, buf->size()) {} + byte_view(byte_array buf, uint32_t offset, uint32_t sz) { m_base_buf = std::move(buf); - m_view.bytes = m_base_buf->bytes + offset; - m_view.size = sz; + m_view.set_bytes(m_base_buf->cbytes() + offset); + m_view.set_size(sz); } - byte_view(const byte_view& v, const uint32_t offset, const uint32_t sz) { - DEBUG_ASSERT_GE(v.m_view.size, sz + offset); + byte_view(const byte_view& v, uint32_t offset, uint32_t sz) { + DEBUG_ASSERT_GE(v.m_view.size(), sz + offset); m_base_buf = v.m_base_buf; - m_view.bytes = v.m_view.bytes + offset; - m_view.size = sz; + m_view.set_bytes(v.m_view.cbytes() + offset); + m_view.set_size(sz); } - byte_view(const sisl::io_blob& blob) : - byte_view(std::make_shared< byte_array_impl >(blob.bytes, blob.size, blob.aligned)) {} + byte_view(const sisl::io_blob& b) : byte_view(b.size(), b.is_aligned()) {} ~byte_view() = default; byte_view(const byte_view& other) = default; @@ -289,33 +415,38 @@ struct byte_view { } blob get_blob() const { return m_view; } - uint8_t* bytes() const { return m_view.bytes; } - uint32_t size() const { return m_view.size; } - void move_forward(const uint32_t by) { - assert(m_view.size >= by); - m_view.bytes += by; - m_view.size -= by; + uint8_t const* bytes() const { return m_view.cbytes(); } + uint32_t size() const { return m_view.size(); } + void move_forward(uint32_t by) { + DEBUG_ASSERT_GE(m_view.size(), by, "Size greater than move forward request by"); + m_view.set_bytes(m_view.cbytes() + by); + m_view.set_size(m_view.size() - by); validate(); } // Extract the byte_array so that caller can safely use the underlying byte_array. If the view represents the // entire array, it will not do any copy. If view represents only portion of array, create a copy of the byte array // and returns that value - byte_array extract(const uint32_t alignment = 0) const { + byte_array extract(uint32_t alignment = 0) const { if (can_do_shallow_copy()) { return m_base_buf; } else { - auto base_buf = make_byte_array(m_view.size, alignment, m_base_buf->m_tag); - std::memcpy(base_buf->bytes, m_view.bytes, m_view.size); + auto base_buf = make_byte_array(m_view.size(), alignment, m_base_buf->m_tag); + std::memcpy(base_buf->bytes(), m_view.cbytes(), m_view.size()); return base_buf; } } bool can_do_shallow_copy() const { - return (m_view.bytes == m_base_buf->bytes) && (m_view.size == m_base_buf->size); + return (m_view.cbytes() == m_base_buf->cbytes()) && (m_view.size() == m_base_buf->size()); + } + void set_size(uint32_t sz) { m_view.set_size(sz); } + void validate() const { + DEBUG_ASSERT_LE((void*)(m_base_buf->cbytes() + m_base_buf->size()), (void*)(m_view.cbytes() + m_view.size()), + "Invalid byte_view"); } - void set_size(const uint32_t sz) { m_view.size = sz; } - void validate() { assert((m_base_buf->bytes + m_base_buf->size) >= (m_view.bytes + m_view.size)); } + + std::string get_string() const { return std::string(r_cast< const char* >(bytes()), uint64_cast(size())); } private: byte_array m_base_buf; diff --git a/include/sisl/fds/compact_bitset.hpp b/include/sisl/fds/compact_bitset.hpp new file mode 100644 index 00000000..2c8f5db6 --- /dev/null +++ b/include/sisl/fds/compact_bitset.hpp @@ -0,0 +1,175 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include + +namespace sisl { +class CompactBitSet { +public: + using bit_count_t = uint32_t; + +private: + using bitword_type = Bitword< unsafe_bits< uint64_t > >; + + struct serialized { + bitword_type words[1]{bitword_type{}}; + }; + + bit_count_t nbits_{0}; + bool allocated_{false}; + serialized* s_{nullptr}; + +private: + static constexpr size_t word_size_bytes() { return sizeof(unsafe_bits< uint64_t >); } + static constexpr size_t word_size_bits() { return word_size_bytes() * 8; } + static constexpr uint64_t word_mask() { return bitword_type::bits() - 1; } + +public: + static constexpr bit_count_t inval_bit = std::numeric_limits< bit_count_t >::max(); + static constexpr uint8_t size_multiples() { return word_size_bytes(); } + + explicit CompactBitSet(bit_count_t nbits) { + DEBUG_ASSERT_GT(nbits, 0, "compact bitset should have nbits > 0"); + nbits_ = s_cast< bit_count_t >(sisl::round_up(nbits, word_size_bits())); + size_t const buf_size = nbits_ / 8; + + uint8_t* buf = new uint8_t[buf_size]; + std::memset(buf, 0, buf_size); + s_ = r_cast< serialized* >(buf); + allocated_ = true; + } + + CompactBitSet(sisl::blob buf, bool init_bits) : s_{r_cast< serialized* >(buf.bytes())} { + DEBUG_ASSERT_GT(buf.size(), 0, "compact bitset initialized with empty buffer"); + DEBUG_ASSERT_EQ(buf.size() % word_size_bytes(), 0, "compact bitset buffer size must be multiple of word size"); + nbits_ = buf.size() * 8; + if (init_bits) { std::memset(buf.bytes(), 0, buf.size()); } + } + + ~CompactBitSet() { + if (allocated_) { delete[] uintptr_cast(s_); } + } + + bit_count_t size() const { return nbits_; } + void set_bit(bit_count_t start) { set_reset_bit(start, true); } + void reset_bit(bit_count_t start) { set_reset_bit(start, false); } + + bool is_bit_set(bit_count_t bit) const { + bitword_type const* word_ptr = get_word_const(bit); + if (!word_ptr) { return false; } + uint8_t const offset = get_word_offset(bit); + return word_ptr->is_bit_set_reset(offset, true); + } + + bit_count_t get_next_set_bit(bit_count_t start_bit) const { return get_next_set_or_reset_bit(start_bit, true); } + bit_count_t get_next_reset_bit(bit_count_t start_bit) const { return get_next_set_or_reset_bit(start_bit, false); } + + /// @brief This method gets the previous set bit from starting bit (including the start bit). So if start bit + /// is 1, it will return the start bit. + /// @param start_bit: Start bit should be > 0 and <= size() + /// @return Returns the previous set bit or inval_bit if nothing is set + bit_count_t get_prev_set_bit(bit_count_t start_bit) const { + // check first word which may be partial + uint8_t offset = get_word_offset(start_bit); + bit_count_t word_idx = get_word_index(start_bit); + + do { + bitword_type const* word_ptr = &s_->words[word_idx]; + if (!word_ptr) { return inval_bit; } + + uint8_t nbit{0}; + if (word_ptr->get_prev_set_bit(offset, &nbit)) { return start_bit - (offset - nbit); } + + start_bit -= offset; + offset = bitword_type::bits(); + } while (word_idx-- != 0); + + return inval_bit; + } + + void set_reset_bit(bit_count_t bit, bool value) { + bitword_type* word_ptr = get_word(bit); + if (!word_ptr) { return; } + uint8_t const offset = get_word_offset(bit); + word_ptr->set_reset_bits(offset, 1, value); + } + + bit_count_t get_next_set_or_reset_bit(bit_count_t start_bit, bool search_for_set_bit) const { + bit_count_t ret{inval_bit}; + + // check first word which may be partial + uint8_t const offset = get_word_offset(start_bit); + bitword_type const* word_ptr = get_word_const(start_bit); + if (!word_ptr) { return ret; } + + uint8_t nbit{0}; + bool found = search_for_set_bit ? word_ptr->get_next_set_bit(offset, &nbit) + : word_ptr->get_next_reset_bit(offset, &nbit); + if (found) { ret = start_bit + nbit - offset; } + + if (ret == inval_bit) { + // test rest of whole words + bit_count_t current_bit = start_bit + (bitword_type::bits() - offset); + bit_count_t bits_remaining = (current_bit > size()) ? 0 : size() - current_bit; + while (bits_remaining > 0) { + ++word_ptr; + found = + search_for_set_bit ? word_ptr->get_next_set_bit(0, &nbit) : word_ptr->get_next_reset_bit(0, &nbit); + if (found) { + ret = current_bit + nbit; + break; + } + current_bit += bitword_type::bits(); + bits_remaining -= std::min< bit_count_t >(bits_remaining, bitword_type::bits()); + } + } + + if (ret >= size()) { ret = inval_bit; } + return ret; + } + + std::string to_string() const { + std::string str; + auto const num_words = size() / word_size_bits(); + for (uint32_t i{0}; i < num_words; ++i) { + fmt::format_to(std::back_inserter(str), "{}", s_->words[i].to_string()); + } + return str; + } + +private: + bitword_type* get_word(bit_count_t bit) { + return (sisl_unlikely(bit >= nbits_)) ? nullptr : &s_->words[bit / word_size_bits()]; + } + + bitword_type const* get_word_const(bit_count_t bit) const { + return (sisl_unlikely(bit >= nbits_)) ? nullptr : &s_->words[bit / word_size_bits()]; + } + + bit_count_t get_word_index(bit_count_t bit) const { + DEBUG_ASSERT(s_, "compact bitset not initialized"); + return bit / word_size_bits(); + } + + uint8_t get_word_offset(bit_count_t bit) const { + assert(s_); + return static_cast< uint8_t >(bit & word_mask()); + } +}; +} // namespace sisl diff --git a/src/fds/compress.hpp b/include/sisl/fds/compress.hpp similarity index 100% rename from src/fds/compress.hpp rename to include/sisl/fds/compress.hpp diff --git a/include/sisl/fds/concurrent_insert_vector.hpp b/include/sisl/fds/concurrent_insert_vector.hpp new file mode 100644 index 00000000..f9d4fd79 --- /dev/null +++ b/include/sisl/fds/concurrent_insert_vector.hpp @@ -0,0 +1,122 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include + +#include + +namespace sisl { + +// +// This data structure provides a vector where concurrent threads can safely emplace or push back the data into. +// However, it does not guarantee any access or iterations happen during the insertion. It is the responsibility of the +// user to synchornoize this behavior. This data structure is useful when the user wants to insert data into a vector +// concurrently in a fast manner and then iterate over the data later. If the user wants a vector implementation which +// reads concurrently with writer, they can use sisl::ThreadVector. This data structure is provided as a replacement for +// simplistic cases where insertion and iteration never happen concurrently. As a result it provides better performance +// than even sisl::ThreadVector and better debuggability. +// +// Benchmark shows atleast 10x better performance on more than 4 threads concurrently inserting with mutex. +// +template < typename T > +class ConcurrentInsertVector { +private: + ExitSafeThreadBuffer< std::vector< T >, size_t > tvector_; + std::vector< std::vector< T > const* > per_thread_vec_ptrs_; + +public: + struct iterator { + size_t next_thread{0}; + size_t next_id_in_thread{0}; + ConcurrentInsertVector const* vec{nullptr}; + + iterator() = default; + iterator(ConcurrentInsertVector const& v) : vec{&v} {} + iterator(ConcurrentInsertVector const& v, bool end_iterator) : vec{&v} { + if (end_iterator) { next_thread = vec->per_thread_vec_ptrs_.size(); } + } + + void operator++() { + ++next_id_in_thread; + if (next_id_in_thread >= vec->per_thread_vec_ptrs_[next_thread]->size()) { + ++next_thread; + next_id_in_thread = 0; + } + } + + bool operator==(iterator const& other) const = default; + bool operator!=(iterator const& other) const = default; + + T const& operator*() const { return vec->per_thread_vec_ptrs_[next_thread]->at(next_id_in_thread); } + T const* operator->() const { return &(vec->per_thread_vec_ptrs_[next_thread]->at(next_id_in_thread)); } + }; + + ConcurrentInsertVector() = default; + ConcurrentInsertVector(size_t size) : tvector_{size} {} + ConcurrentInsertVector(const ConcurrentInsertVector&) = delete; + ConcurrentInsertVector(ConcurrentInsertVector&&) noexcept = delete; + ConcurrentInsertVector& operator=(const ConcurrentInsertVector&) = delete; + ConcurrentInsertVector& operator=(ConcurrentInsertVector&&) noexcept = delete; + ~ConcurrentInsertVector() = default; + + template < typename InputType, + typename = typename std::enable_if< + std::is_convertible< typename std::decay< InputType >::type, T >::value >::type > + void push_back(InputType&& ele) { + tvector_->push_back(std::forward< InputType >(ele)); + } + + template < class... Args > + void emplace_back(Args&&... args) { + tvector_->emplace_back(std::forward< Args >(args)...); + } + + iterator begin() { + tvector_.access_all_threads([this](std::vector< T > const* tvec, bool, bool) { + if (tvec && tvec->size()) { per_thread_vec_ptrs_.push_back(tvec); } + return false; + }); + return iterator{*this}; + } + + iterator end() { return iterator{*this, true /* end_iterator */}; } + + void foreach_entry(auto&& cb) { + tvector_.access_all_threads([this, &cb](std::vector< T > const* tvec, bool, bool) { + if (tvec) { + for (auto const& e : *tvec) { + cb(e); + } + } + return false; + }); + } + + size_t size() const { + size_t sz{0}; + const_cast< ExitSafeThreadBuffer< std::vector< T >, size_t >& >(tvector_).access_all_threads( + [this, &sz](std::vector< T > const* tvec, bool, bool) { + if (tvec) { sz += tvec->size(); } + return false; + }); + return sz; + } +}; + +} // namespace sisl diff --git a/src/fds/flexarray.hpp b/include/sisl/fds/flexarray.hpp similarity index 100% rename from src/fds/flexarray.hpp rename to include/sisl/fds/flexarray.hpp diff --git a/src/fds/freelist_allocator.hpp b/include/sisl/fds/freelist_allocator.hpp similarity index 99% rename from src/fds/freelist_allocator.hpp rename to include/sisl/fds/freelist_allocator.hpp index 9b3fb57c..60879f39 100644 --- a/src/fds/freelist_allocator.hpp +++ b/include/sisl/fds/freelist_allocator.hpp @@ -33,7 +33,7 @@ #pragma GCC diagnostic pop #endif -#include "metrics/metrics.hpp" +#include #include "utils.hpp" namespace sisl { diff --git a/src/fds/id_reserver.hpp b/include/sisl/fds/id_reserver.hpp similarity index 99% rename from src/fds/id_reserver.hpp rename to include/sisl/fds/id_reserver.hpp index 4c123a2f..b18f592b 100644 --- a/src/fds/id_reserver.hpp +++ b/include/sisl/fds/id_reserver.hpp @@ -21,8 +21,6 @@ #include #include -#include - #include "bitset.hpp" #include "utils.hpp" diff --git a/src/fds/malloc_helper.hpp b/include/sisl/fds/malloc_helper.hpp similarity index 97% rename from src/fds/malloc_helper.hpp rename to include/sisl/fds/malloc_helper.hpp index e0081353..d5d31967 100644 --- a/src/fds/malloc_helper.hpp +++ b/include/sisl/fds/malloc_helper.hpp @@ -36,11 +36,11 @@ #include #endif -#include "logging/logging.h" #include -#include "metrics/histogram_buckets.hpp" -#include "metrics/metrics.hpp" +#include +#include +#include #if defined(USING_TCMALLOC) #include @@ -297,7 +297,7 @@ static size_t get_jemalloc_muzzy_page_count() { #endif /* Get the application total allocated memory. Relies on jemalloc. Returns 0 for other allocator. */ -[[maybe_unused]] static size_t get_total_memory(const bool refresh = true) { +[[maybe_unused]] static size_t get_total_memory([[maybe_unused]] const bool refresh = true) { size_t allocated{0}; #ifndef USING_TCMALLOC @@ -321,7 +321,7 @@ static size_t get_jemalloc_muzzy_page_count() { } #if defined(USING_TCMALLOC) -static void update_tcmalloc_range_stats(void* const arg, const base::MallocRange* const range) { +static void update_tcmalloc_range_stats([[maybe_unused]] void* const arg, const base::MallocRange* const range) { // LOGINFO("Range: address={}, length={}, Type={}, fraction={}", range->address, range->length, range->type, // range->fraction); @@ -569,7 +569,7 @@ static void print_my_jemalloc_data(void* const opaque, const char* const buf) { #endif #endif -[[maybe_unused]] static bool set_memory_release_rate(const double level) { +[[maybe_unused]] static bool set_memory_release_rate([[maybe_unused]] const double level) { #if defined(USING_TCMALLOC) MallocExtension::instance()->SetMemoryReleaseRate(level); return true; @@ -599,8 +599,8 @@ static std::atomic< bool > s_is_aggressive_decommit{false}; return true; } -[[maybe_unused]] static bool reset_aggressive_decommit_mem_if_needed(const size_t mem_usage, - const size_t aggressive_threshold) { +[[maybe_unused]] static bool +reset_aggressive_decommit_mem_if_needed([[maybe_unused]] const size_t mem_usage, [[maybe_unused]] const size_t aggressive_threshold) { #if defined(USING_TCMALLOC) if (tcmalloc_helper::s_is_aggressive_decommit.load(std::memory_order_acquire)) { LOGINFO("Total memory alloced={} is restored back to less than aggressive threshold limit {}, " @@ -628,7 +628,7 @@ static std::atomic< bool > s_is_aggressive_decommit{false}; return true; } -[[maybe_unused]] static bool release_mem_if_needed(const size_t soft_threshold, const size_t aggressive_threshold_in) { +[[maybe_unused]] static bool release_mem_if_needed([[maybe_unused]] const size_t soft_threshold, [[maybe_unused]] const size_t aggressive_threshold_in) { bool ret{false}; #if defined(USING_TCMALLOC) || defined(USING_JEMALLOC) || defined(USE_JEMALLOC) size_t mem_usage{0}; diff --git a/src/fds/obj_allocator.hpp b/include/sisl/fds/obj_allocator.hpp similarity index 100% rename from src/fds/obj_allocator.hpp rename to include/sisl/fds/obj_allocator.hpp diff --git a/src/fds/sparse_vector.hpp b/include/sisl/fds/sparse_vector.hpp similarity index 100% rename from src/fds/sparse_vector.hpp rename to include/sisl/fds/sparse_vector.hpp diff --git a/src/fds/stream_tracker.hpp b/include/sisl/fds/stream_tracker.hpp similarity index 85% rename from src/fds/stream_tracker.hpp rename to include/sisl/fds/stream_tracker.hpp index 3366acb2..3cff7e64 100644 --- a/src/fds/stream_tracker.hpp +++ b/include/sisl/fds/stream_tracker.hpp @@ -16,10 +16,11 @@ *********************************************************************************/ #pragma once +#include +#include +#include + #include "bitset.hpp" -#include "folly/SharedMutex.h" -#include "../metrics/metrics_group_impl.hpp" -#include "../metrics/metrics.hpp" namespace sisl { class StreamTrackerMetrics : public MetricsGroupWrapper { @@ -43,7 +44,7 @@ class StreamTracker { public: static constexpr size_t alloc_blk_size = 10000; static constexpr size_t compaction_threshold = alloc_blk_size / 2; - static constexpr auto null_processor = [](auto... x) -> bool { return true; }; + static constexpr auto null_processor = []([[maybe_unused]] auto... x) -> bool { return true; }; static_assert(std::is_trivially_copyable< T >::value, "Cannot use StreamTracker for non-trivally copyable classes"); @@ -73,7 +74,7 @@ class StreamTracker { template < class... Args > int64_t create(int64_t idx, Args&&... args) { return do_update( - idx, [](T& data) { return false; }, true /* replace */, std::forward< Args >(args)...); + idx, []([[maybe_unused]] T& data) { return false; }, true /* replace */, std::forward< Args >(args)...); } template < class... Args > @@ -87,6 +88,18 @@ class StreamTracker { m_comp_slot_bits.set_bits(start_bit, end_idx - start_idx + 1); } + void rollback(int64_t new_end_idx) { + folly::SharedMutexWritePriority::ReadHolder holder(m_lock); + if ((new_end_idx < m_slot_ref_idx) || + (new_end_idx >= (m_slot_ref_idx + int64_cast(m_active_slot_bits.size())))) { + throw std::out_of_range("Slot idx is not in range"); + } + + auto new_end_bit = new_end_idx - m_slot_ref_idx; + m_active_slot_bits.reset_bits(new_end_bit + 1, m_active_slot_bits.size() - new_end_bit - 1); + m_comp_slot_bits.reset_bits(new_end_bit + 1, m_comp_slot_bits.size() - new_end_bit - 1); + } + T& at(int64_t idx) const { folly::SharedMutexWritePriority::ReadHolder holder(m_lock); if (idx < m_slot_ref_idx) { throw std::out_of_range("Slot idx is not in range"); } @@ -175,9 +188,10 @@ class StreamTracker { return m_slot_ref_idx - 1; } - void foreach_completed(int64_t start_idx, const auto& cb) { _foreach(start_idx, true /* completed */, cb); } - - void foreach_active(int64_t start_idx, const auto& cb) { _foreach(start_idx, false /* completed */, cb); } + void foreach_contiguous_completed(int64_t start_idx, const auto& cb) { _foreach_contiguous(start_idx, true, cb); } + void foreach_contiguous_active(int64_t start_idx, const auto& cb) { _foreach_contiguous(start_idx, false, cb); } + void foreach_all_completed(int64_t start_idx, const auto& cb) { _foreach_all(start_idx, true, cb); } + void foreach_all_active(int64_t start_idx, const auto& cb) { _foreach_all(start_idx, false, cb); } int64_t completed_upto(int64_t search_hint_idx = 0) const { folly::SharedMutexWritePriority::ReadHolder holder(m_lock); @@ -294,15 +308,27 @@ class StreamTracker { } } - void _foreach(int64_t start_idx, bool completed, const auto& cb) { + void _foreach_contiguous(int64_t start_idx, bool completed_only, const auto& cb) { folly::SharedMutexWritePriority::ReadHolder holder(m_lock); - auto upto = _upto(completed, start_idx); + auto upto = _upto(completed_only, start_idx); for (auto idx = start_idx; idx <= upto; ++idx) { auto proceed = cb(idx, upto, *(get_slot_data(idx - m_slot_ref_idx))); if (!proceed) break; } } + void _foreach_all(int64_t start_idx, bool completed_only, const auto& cb) { + folly::SharedMutexWritePriority::ReadHolder holder(m_lock); + auto search_bit = std::max(0l, (start_idx - m_slot_ref_idx)); + do { + search_bit = completed_only ? m_comp_slot_bits.get_next_set_bit(search_bit) + : m_active_slot_bits.get_next_set_bit(search_bit); + if (search_bit == AtomicBitset::npos) { break; } + if (!cb(search_bit + m_slot_ref_idx, *(get_slot_data(search_bit)))) { break; } + ++search_bit; + } while (true); + } + T* get_slot_data(int64_t nbit) const { return &(m_slot_data[nbit + m_data_skip_count]); } private: diff --git a/src/fds/thread_vector.hpp b/include/sisl/fds/thread_vector.hpp similarity index 97% rename from src/fds/thread_vector.hpp rename to include/sisl/fds/thread_vector.hpp index 3628e90d..59e335a9 100644 --- a/src/fds/thread_vector.hpp +++ b/include/sisl/fds/thread_vector.hpp @@ -20,8 +20,8 @@ #include #include -#include "wisr/wisr_framework.hpp" -#include "wisr/wisr_ds.hpp" +#include +#include namespace sisl { diff --git a/src/fds/utils.hpp b/include/sisl/fds/utils.hpp similarity index 96% rename from src/fds/utils.hpp rename to include/sisl/fds/utils.hpp index 094c1df7..6615103e 100644 --- a/src/fds/utils.hpp +++ b/include/sisl/fds/utils.hpp @@ -28,8 +28,8 @@ #include #include -#include "boost/preprocessor/arithmetic/inc.hpp" -#include "boost/preprocessor/repetition/repeat_from_to.hpp" +#include +#include // NOTE: In future should use [[likely]] and [[unlikely]] but not valid syntax in if predicate #if defined __GNUC__ || defined __llvm__ @@ -224,10 +224,14 @@ static int spaceship_oper(const T& left, const T& right) { #define uintptr_cast reinterpret_cast< uint8_t* > #define voidptr_cast reinterpret_cast< void* > +#define c_voidptr_cast reinterpret_cast< const void* > +#define charptr_cast reinterpret_cast< char* > +#define c_charptr_cast reinterpret_cast< const char* > #define int_cast static_cast< int > #define uint32_cast static_cast< uint32_t > #define int64_cast static_cast< int64_t > #define uint64_cast static_cast< uint64_t > +#define size_cast static_cast< size_t > } // namespace sisl diff --git a/src/fds/vector_pool.hpp b/include/sisl/fds/vector_pool.hpp similarity index 100% rename from src/fds/vector_pool.hpp rename to include/sisl/fds/vector_pool.hpp diff --git a/src/file_watcher/file_watcher.hpp b/include/sisl/file_watcher/file_watcher.hpp similarity index 92% rename from src/file_watcher/file_watcher.hpp rename to include/sisl/file_watcher/file_watcher.hpp index 6e7b79c9..109028d0 100644 --- a/src/file_watcher/file_watcher.hpp +++ b/include/sisl/file_watcher/file_watcher.hpp @@ -2,7 +2,7 @@ #pragma once #include -#include "logging/logging.h" +#include #include namespace sisl { @@ -35,13 +35,14 @@ class FileWatcher { void handle_events(); void get_fileinfo(const int wd, FileInfo& file_info) const; void on_modified_event(const int wd, const bool is_deleted); + bool remove_watcher(FileInfo& file_info); static bool get_file_contents(const std::string& file_name, std::string& contents); static bool check_file_size(const std::string& file_path); private: int m_inotify_fd; std::map< std::string, FileInfo > m_files; - std::mutex m_files_lock; + mutable std::mutex m_files_lock; std::unique_ptr< std::thread > m_fw_thread; // This is used to notify poll loop to exit int m_pipefd[2] = {-1, -1}; diff --git a/include/sisl/flip/flip.hpp b/include/sisl/flip/flip.hpp new file mode 100644 index 00000000..ce778cd0 --- /dev/null +++ b/include/sisl/flip/flip.hpp @@ -0,0 +1,699 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#ifndef FLIP_FLIP_HPP +#define FLIP_FLIP_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "proto/flip_spec.pb.h" +#include "flip_rpc_server.hpp" +#include + +SISL_LOGGING_DECL(flip) + +namespace flip { + +template < size_t Index = 0, // start iteration at 0 index + typename TTuple, // the tuple type + size_t Size = std::tuple_size_v< std::remove_reference_t< TTuple > >, // tuple size + typename TCallable, // the callable to bo invoked for each tuple item + typename... TArgs // other arguments to be passed to the callable + > +void for_each(TTuple&& tuple, TCallable&& callable, TArgs&&... args) { + if constexpr (Index < Size) { + std::invoke(callable, args..., std::get< Index >(tuple)); + + if constexpr (Index + 1 < Size) { + for_each< Index + 1 >(std::forward< TTuple >(tuple), std::forward< TCallable >(callable), + std::forward< TArgs >(args)...); + } + } +} + +struct flip_name_compare { + bool operator()(const std::string& lhs, const std::string& rhs) const { return lhs < rhs; } +}; + +struct flip_instance { + flip_instance(const FlipSpec& fspec) : + m_fspec(fspec), m_hit_count(0), m_remain_exec_count(fspec.flip_frequency().count()) {} + + flip_instance(const flip_instance& other) { + m_fspec = other.m_fspec; + m_hit_count.store(other.m_hit_count.load()); + m_remain_exec_count.store(other.m_remain_exec_count.load()); + } + + std::string to_string() const { + std::stringstream ss; + ss << "\n---------------------------" << m_fspec.flip_name() << "-----------------------\n"; + ss << "Hitcount: " << m_hit_count << "\n"; + ss << "Remaining count: " << m_remain_exec_count << "\n"; + ss << m_fspec.flip_frequency().DebugString(); + ss << m_fspec.flip_action().DebugString(); + ss << "Conditions: [\n"; + auto i = 1; + for (const auto& cond : m_fspec.conditions()) { + ss << std::to_string(i) << ") " << Operator_Name(cond.oper()) << " => " << cond.value().DebugString(); + ++i; + } + ss << "]"; + ss << "\n-------------------------------------------------------------------\n"; + return ss.str(); + } + + FlipSpec m_fspec; + std::atomic< uint32_t > m_hit_count; + std::atomic< int32_t > m_remain_exec_count; +}; + +/****************************** Proto Param to Value converter ******************************/ +template < typename T > +struct val_converter { + T operator()(const ParamValue& val) { return 0; } +}; + +template <> +struct val_converter< int > { + int operator()(const ParamValue& val) { return (val.kind_case() == ParamValue::kIntValue) ? val.int_value() : 0; } +}; + +#if 0 +template <> +struct val_converter { + const int operator()(const ParamValue &val) { + return (val.kind_case() == ParamValue::kIntValue) ? val.int_value() : 0; + } +}; +#endif + +template <> +struct val_converter< long > { + long operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kLongValue) ? val.long_value() : 0; + } +}; + +template <> +struct val_converter< double > { + double operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kDoubleValue) ? val.double_value() : 0; + } +}; + +template <> +struct val_converter< std::string > { + std::string operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kStringValue) ? val.string_value() : ""; + } +}; + +template <> +struct val_converter< const std::string > { + std::string operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kStringValue) ? val.string_value() : ""; + } +}; + +template <> +struct val_converter< const char* > { + const char* operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kStringValue) ? val.string_value().c_str() : nullptr; + } +}; + +template <> +struct val_converter< bool > { + bool operator()(const ParamValue& val) { + return (val.kind_case() == ParamValue::kBoolValue) ? val.bool_value() : 0; + } +}; + +template < typename T > +struct delayed_return_param { + uint64_t delay_usec; + T val; +}; + +template < typename T > +struct val_converter< delayed_return_param< T > > { + delayed_return_param< T > operator()(const ParamValue& val) { + delayed_return_param< T > dummy; + return dummy; + } +}; + +/******************************************** Value to Proto converter ****************************************/ +template < typename T > +struct to_proto_converter { + void operator()(const T& val, ParamValue* out_pval) {} +}; + +template <> +struct to_proto_converter< int > { + void operator()(const int& val, ParamValue* out_pval) { out_pval->set_int_value(val); } +}; + +template <> +struct to_proto_converter< long > { + void operator()(const long& val, ParamValue* out_pval) { out_pval->set_long_value(val); } +}; + +template <> +struct to_proto_converter< double > { + void operator()(const double& val, ParamValue* out_pval) { out_pval->set_double_value(val); } +}; + +template <> +struct to_proto_converter< std::string > { + void operator()(const std::string& val, ParamValue* out_pval) { out_pval->set_string_value(val); } +}; + +template <> +struct to_proto_converter< const std::string > { + void operator()(const std::string& val, ParamValue* out_pval) { out_pval->set_string_value(val); } +}; + +template <> +struct to_proto_converter< const char* > { + void operator()(const char*& val, ParamValue* out_pval) { out_pval->set_string_value(val); } +}; + +template <> +struct to_proto_converter< bool > { + void operator()(const bool& val, ParamValue* out_pval) { out_pval->set_bool_value(val); } +}; + +/******************************************* Comparators *************************************/ +template < typename T > +struct compare_val { + bool operator()(const T& val1, const T& val2, Operator oper) { + switch (oper) { + case Operator::DONT_CARE: + return true; + + case Operator::EQUAL: + return (val1 == val2); + + case Operator::NOT_EQUAL: + return (val1 != val2); + + case Operator::GREATER_THAN: + return (val1 > val2); + + case Operator::LESS_THAN: + return (val1 < val2); + + case Operator::GREATER_THAN_OR_EQUAL: + return (val1 >= val2); + + case Operator::LESS_THAN_OR_EQUAL: + return (val1 <= val2); + + default: + return false; + } + } +}; + +template <> +struct compare_val< std::string > { + bool operator()(const std::string& val1, const std::string& val2, Operator oper) { + switch (oper) { + case Operator::DONT_CARE: + return true; + + case Operator::EQUAL: + return (val1 == val2); + + case Operator::NOT_EQUAL: + return (val1 != val2); + + case Operator::GREATER_THAN: + return (val1 > val2); + + case Operator::LESS_THAN: + return (val1 < val2); + + case Operator::GREATER_THAN_OR_EQUAL: + return (val1 >= val2); + + case Operator::LESS_THAN_OR_EQUAL: + return (val1 <= val2); + + case Operator::REG_EX: { + const std::regex re(val2); + return (std::sregex_iterator(val1.begin(), val1.end(), re) != std::sregex_iterator()); + } + + default: + return false; + } + } +}; + +template <> +struct compare_val< const std::string > { + bool operator()(const std::string& val1, const std::string& val2, Operator oper) { + switch (oper) { + case Operator::DONT_CARE: + return true; + + case Operator::EQUAL: + return (val1 == val2); + + case Operator::NOT_EQUAL: + return (val1 != val2); + + case Operator::GREATER_THAN: + return (val1 > val2); + + case Operator::LESS_THAN: + return (val1 < val2); + + case Operator::GREATER_THAN_OR_EQUAL: + return (val1 >= val2); + + case Operator::LESS_THAN_OR_EQUAL: + return (val1 <= val2); + + case Operator::REG_EX: { + const std::regex re(val2); + return (std::sregex_iterator(val1.begin(), val1.end(), re) != std::sregex_iterator()); + } + + default: + return false; + } + } +}; + +template <> +struct compare_val< const char* > { + bool operator()(const char*& val1, const char*& val2, Operator oper) { + switch (oper) { + case Operator::DONT_CARE: + return true; + + case Operator::EQUAL: + return (val1 && val2 && (strcmp(val1, val2) == 0)) || (!val1 && !val2); + + case Operator::NOT_EQUAL: + return (val1 && val2 && (strcmp(val1, val2) != 0)) || (!val1 && val2) || (val1 && !val2); + + case Operator::GREATER_THAN: + return (val1 && val2 && (strcmp(val1, val2) > 0)) || (val1 && !val2); + + case Operator::LESS_THAN: + return (val1 && val2 && (strcmp(val1, val2) < 0)) || (!val1 && val2); + + case Operator::GREATER_THAN_OR_EQUAL: + return (val1 && val2 && (strcmp(val1, val2) >= 0)) || (val1 && !val2) || (!val1 && !val2); + + case Operator::LESS_THAN_OR_EQUAL: + return (val1 && val2 && (strcmp(val1, val2) <= 0)) || (!val1 && val2) || (!val1 && !val2); + + case Operator::REG_EX: { + const std::regex re(val2); + const std::string v(val1); + return (std::sregex_iterator(v.begin(), v.end(), re) != std::sregex_iterator()); + } + + default: + return false; + } + } +}; + +using io_service = boost::asio::io_service; +using deadline_timer = boost::asio::deadline_timer; +using io_work = boost::asio::io_service::work; + +class FlipTimerBase { +public: + virtual ~FlipTimerBase() = default; + virtual void schedule(const std::string& timer_name, boost::posix_time::time_duration delay_us, + const std::function< void() >& closure) = 0; + virtual void cancel(const std::string& timer_name) = 0; +}; + +class FlipTimerAsio : public FlipTimerBase { +public: + FlipTimerAsio() {} + ~FlipTimerAsio() { + if (m_timer_thread != nullptr) { + m_work.reset(); + m_timer_thread->join(); + } + } + + void schedule(const std::string& timer_name, boost::posix_time::time_duration delay_us, + const std::function< void() >& closure) override { + std::unique_lock< std::mutex > lk(m_thr_mutex); + if (m_work == nullptr) { + m_work = std::make_unique< io_work >(m_svc); + m_timer_thread = std::make_unique< std::thread >(std::bind(&FlipTimerAsio::timer_thr, this)); + } + + auto t = std::make_shared< deadline_timer >(m_svc, delay_us); + t->async_wait([this, closure, t, timer_name](const boost::system::error_code& e) { + if (e) { + LOGERRORMOD(flip, "Error in timer routine, message {}", e.message()); + } else { + closure(); + } + remove_timer(timer_name, t); + }); + + m_timer_instances.insert(std::make_pair(timer_name, std::move(t))); + } + + void cancel(const std::string& timer_name) { remove_timer(timer_name, nullptr); } + + void timer_thr() { + size_t executed = 0; + executed = m_svc.run(); + // To suppress compiler warning + (void)executed; + } + +private: + void remove_timer(const std::string& timer_name, const std::shared_ptr< deadline_timer >& t) { + std::unique_lock< std::mutex > lk(m_thr_mutex); + auto range = m_timer_instances.equal_range(timer_name); + for (auto it = range.first; it != range.second;) { + if ((t == nullptr) || (it->second == t)) { + it = m_timer_instances.erase(it); + } else { + ++it; + } + } + } + +private: + io_service m_svc; + std::unique_ptr< io_work > m_work; + std::mutex m_thr_mutex; + std::unique_ptr< std::thread > m_timer_thread; + std::multimap< std::string, std::shared_ptr< deadline_timer > > m_timer_instances; +}; + +static constexpr int TEST_ONLY = 0; +static constexpr int RETURN_VAL = 1; +static constexpr int SET_DELAY = 2; +static constexpr int DELAYED_RETURN = 3; + +class Flip { +public: + Flip() : m_flip_enabled(false) {} + ~Flip() { + if (m_flip_server) { stop_rpc_server(); } + } + + static Flip& instance() { + static Flip s_instance; + return s_instance; + } + + void start_rpc_server() { + if (m_flip_server) { stop_rpc_server(); } + + m_flip_server = std::make_unique< FlipRPCServer >(); + std::string server_address("0.0.0.0:50051"); + grpc::ServerBuilder builder; + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + builder.RegisterService((FlipRPCServer::Service*)m_flip_server.get()); + m_grpc_server = builder.BuildAndStart(); + LOGINFOMOD(flip, "Flip GRPC Server listening on {}", server_address); + m_flip_server_thread = std::unique_ptr< std::thread >( + new std::thread([grpc_server = m_grpc_server.get()]() { grpc_server->Wait(); })); + } + + void stop_rpc_server() { + if (m_grpc_server) { m_grpc_server->Shutdown(); } + m_flip_server_thread->join(); + m_flip_server_thread.reset(); + m_flip_server.reset(); + } + + bool add(const FlipSpec& fspec) { + m_flip_enabled = true; + auto inst = flip_instance(fspec); + + // LOG(INFO) << "Fpsec: " << fspec.DebugString(); + + // TODO: Add verification to see if the flip is already scheduled, any errors etc.. + std::unique_lock< std::shared_mutex > lock(m_mutex); + + // Create a timer instance only when we have delays/delayreturns flip added + auto action_type = fspec.flip_action().action_case(); + if ((action_type == FlipAction::kDelays) || (action_type == FlipAction::kDelayReturns)) { + if (m_timer == nullptr) { m_timer = std::make_unique< FlipTimerAsio >(); } + } + m_flip_specs.emplace(std::pair< std::string, flip_instance >(fspec.flip_name(), inst)); + LOGDEBUGMOD(flip, "Added new fault flip {} to the list of flips", fspec.flip_name()); + // LOG(INFO) << "Flip details:" << inst.to_string(); + return true; + } + + std::vector< std::string > get(const std::string& flip_name) { + std::shared_lock< std::shared_mutex > lock(m_mutex); + std::vector< std::string > res; + + auto search = m_flip_specs.equal_range(flip_name); + for (auto it = search.first; it != search.second; ++it) { + const auto& inst = it->second; + res.emplace_back(inst.to_string()); + } + + return res; + } + + std::vector< std::string > get_all() { + std::shared_lock< std::shared_mutex > lock(m_mutex); + std::vector< std::string > res; + + for (const auto& it : m_flip_specs) { + const auto& inst = it.second; + res.emplace_back(inst.to_string()); + } + + return res; + } + + uint32_t remove(const std::string& flip_name) { + std::unique_lock< std::shared_mutex > lock(m_mutex); + auto nremoved = m_flip_specs.erase(flip_name); + m_timer->cancel(flip_name); + return static_cast< uint32_t >(nremoved); + } + + template < class... Args > + bool test_flip(std::string flip_name, Args&&... args) { + if (!m_flip_enabled) return false; + auto ret = __test_flip< bool, TEST_ONLY >(flip_name, std::forward< Args >(args)...); + return (ret != boost::none); + } + + template < typename T, class... Args > + boost::optional< T > get_test_flip(std::string flip_name, Args&&... args) { + if (!m_flip_enabled) return boost::none; + + auto ret = __test_flip< T, RETURN_VAL >(flip_name, std::forward< Args >(args)...); + if (ret == boost::none) return boost::none; + return boost::optional< T >(boost::get< T >(ret.get())); + } + + template < class... Args > + bool delay_flip(std::string flip_name, const std::function< void() >& closure, Args&&... args) { + if (!m_flip_enabled) return false; + + auto ret = __test_flip< bool, SET_DELAY >(flip_name, std::forward< Args >(args)...); + if (ret == boost::none) return false; // Not a hit + + uint64_t delay_usec = boost::get< uint64_t >(ret.get()); + get_timer().schedule(flip_name, boost::posix_time::microseconds(delay_usec), closure); + return true; + } + + template < typename T, class... Args > + bool get_delay_flip(std::string flip_name, const std::function< void(T) >& closure, Args&&... args) { + if (!m_flip_enabled) return false; + + auto ret = __test_flip< T, DELAYED_RETURN >(flip_name, std::forward< Args >(args)...); + if (ret == boost::none) return false; // Not a hit + + auto param = boost::get< delayed_return_param< T > >(ret.get()); + LOGDEBUGMOD(flip, "Returned param delay = {} val = {}", param.delay_usec, param.val); + get_timer().schedule(flip_name, boost::posix_time::microseconds(param.delay_usec), + [closure, param]() { closure(param.val); }); + return true; + } + + void override_timer(std::unique_ptr< FlipTimerBase > t) { + std::unique_lock< std::shared_mutex > lock(m_mutex); + m_timer = std::move(t); + } + +private: + template < typename T, int ActionType, class... Args > + boost::optional< boost::variant< T, bool, uint64_t, delayed_return_param< T > > > __test_flip(std::string flip_name, + Args&&... args) { + bool exec_completed = false; // If all the exec for the flip is completed. + flip_instance* inst = nullptr; + + { + std::shared_lock< std::shared_mutex > lock(m_mutex); + inst = match_flip(flip_name, std::forward< Args >(args)...); + if (inst == nullptr) { + // LOG(INFO) << "Flip " << flip_name << " either not exist or conditions not match"; + return boost::none; + } + auto& fspec = inst->m_fspec; + + // Check if we are subjected to rate limit + if (!handle_hits(fspec.flip_frequency(), inst)) { + LOGDEBUGMOD(flip, "Flip {} matches, but it is rate limited", flip_name); + return boost::none; + } + + // Have we already executed this enough times + auto remain_count = inst->m_remain_exec_count.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (remain_count == 0) { + exec_completed = true; + } else if (remain_count < 0) { + LOGDEBUGMOD(flip, "Flip {} matches, but reaches max count", flip_name); + return boost::none; + } + LOGDEBUGMOD(flip, "Flip {} matches and hits", flip_name); + } + + boost::variant< T, bool, uint64_t, delayed_return_param< T > > val_ret; + switch (inst->m_fspec.flip_action().action_case()) { + case FlipAction::kReturns: + if (ActionType == RETURN_VAL) { + val_ret = val_converter< T >()(inst->m_fspec.flip_action().returns().retval()); + } else { + val_ret = true; + } + break; + + case FlipAction::kNoAction: + // static_assert(!std::is_same::value || std::is_same::value, "__test_flip + // without value should be called with bool as type"); + val_ret = true; + break; + + case FlipAction::kDelays: + val_ret = inst->m_fspec.flip_action().delays().delay_in_usec(); + break; + + case FlipAction::kDelayReturns: + if (ActionType == DELAYED_RETURN) { + auto& flip_dr = inst->m_fspec.flip_action().delay_returns(); + delayed_return_param< T > dr; + dr.delay_usec = flip_dr.delay_in_usec(); + dr.val = val_converter< T >()(flip_dr.retval()); + val_ret = dr; + } else { + val_ret = true; + } + break; + + default: + val_ret = true; + } + + if (exec_completed) { + // If we completed the execution, need to remove them + std::unique_lock< std::shared_mutex > lock(m_mutex); + if (inst->m_remain_exec_count.load(std::memory_order_relaxed) == 0) { m_flip_specs.erase(flip_name); } + } + return val_ret; + } + + template < class... Args > + flip_instance* match_flip(std::string flip_name, Args&&... args) { + flip_instance* match_inst = nullptr; + + auto search = m_flip_specs.equal_range(flip_name); + for (auto it = search.first; it != search.second; ++it) { + auto inst = &it->second; + auto fspec = inst->m_fspec; + + // Check for all the condition match + std::tuple< Args... > arglist(std::forward< Args >(args)...); + auto i = 0U; + bool matched = true; + for_each(arglist, [this, fspec, &i, &matched](auto& v) { + if (!condition_matches(v, fspec.conditions()[i++])) { matched = false; } + }); + + // One or more conditions does not match. + if (matched) { + match_inst = inst; + break; + } + } + return match_inst; + } + + template < typename T > + bool condition_matches(T& comp_val, const FlipCondition& cond) { + auto val1 = val_converter< T >()(cond.value()); + return compare_val< T >()(comp_val, val1, cond.oper()); + } + + bool handle_hits(const FlipFrequency& freq, flip_instance* inst) { + auto hit_count = inst->m_hit_count.fetch_add(1, std::memory_order_release); + if (freq.every_nth() != 0) { + return ((hit_count % freq.every_nth()) == 0); + } else { + return ((uint32_t)(rand() % 100) < freq.percent()); + } + } + + FlipTimerBase& get_timer() { return *m_timer; } + +private: + std::multimap< std::string, flip_instance, flip_name_compare > m_flip_specs; + std::shared_mutex m_mutex; + bool m_flip_enabled; + std::unique_ptr< FlipTimerBase > m_timer; + std::unique_ptr< std::thread > m_flip_server_thread; + std::unique_ptr< FlipRPCServer > m_flip_server; + std::unique_ptr< grpc::Server > m_grpc_server; +}; + +} // namespace flip +#endif // FLIP_FLIP_HPP diff --git a/include/sisl/flip/flip_client.hpp b/include/sisl/flip/flip_client.hpp new file mode 100644 index 00000000..ec258c45 --- /dev/null +++ b/include/sisl/flip/flip_client.hpp @@ -0,0 +1,102 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once +#include "flip.hpp" + +namespace flip { +class FlipClient { +public: + explicit FlipClient(Flip* f) : m_flip(f) {} + + template < typename T > + void create_condition(const std::string& param_name, flip::Operator oper, const T& value, + FlipCondition* out_condition) { + *(out_condition->mutable_name()) = param_name; + out_condition->set_oper(oper); + to_proto_converter< T >()(value, out_condition->mutable_value()); + } + + template < typename T > + FlipCondition create_condition(const std::string& param_name, flip::Operator oper, const T& value) { + FlipCondition fcond; + create_condition(param_name, oper, value, &fcond); + return fcond; + } + + bool inject_noreturn_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq) { + FlipSpec fspec; + + _create_flip_spec(flip_name, conditions, freq, fspec); + fspec.mutable_flip_action()->set_no_action(true); + + m_flip->add(fspec); + return true; + } + + template < typename T > + bool inject_retval_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, const T& retval) { + FlipSpec fspec; + + _create_flip_spec(flip_name, conditions, freq, fspec); + to_proto_converter< T >()(retval, fspec.mutable_flip_action()->mutable_returns()->mutable_retval()); + + m_flip->add(fspec); + return true; + } + + bool inject_delay_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, uint64_t delay_usec) { + FlipSpec fspec; + + _create_flip_spec(flip_name, conditions, freq, fspec); + fspec.mutable_flip_action()->mutable_delays()->set_delay_in_usec(delay_usec); + + m_flip->add(fspec); + return true; + } + + template < typename T > + bool inject_delay_and_retval_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, uint64_t delay_usec, const T& retval) { + FlipSpec fspec; + + _create_flip_spec(flip_name, conditions, freq, fspec); + fspec.mutable_flip_action()->mutable_delays()->set_delay_in_usec(delay_usec); + to_proto_converter< T >()(retval, fspec.mutable_flip_action()->mutable_delay_returns()->mutable_retval()); + + m_flip->add(fspec); + return true; + } + + uint32_t remove_flip(const std::string& flip_name) { return m_flip->remove(flip_name); } + +private: + void _create_flip_spec(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, FlipSpec& out_fspec) { + *(out_fspec.mutable_flip_name()) = flip_name; + for (auto& c : conditions) { + *(out_fspec.mutable_conditions()->Add()) = c; + } + *(out_fspec.mutable_flip_frequency()) = freq; + } + +private: + Flip* m_flip; +}; +} // namespace flip \ No newline at end of file diff --git a/include/sisl/flip/flip_rpc_server.hpp b/include/sisl/flip/flip_rpc_server.hpp new file mode 100644 index 00000000..d3a42ea1 --- /dev/null +++ b/include/sisl/flip/flip_rpc_server.hpp @@ -0,0 +1,33 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once +#include + +#include "proto/flip_spec.pb.h" +#include "proto/flip_server.grpc.pb.h" + +namespace flip { +class FlipRPCServer final : public FlipServer::Service { +public: + FlipRPCServer() = default; + grpc::Status InjectFault(grpc::ServerContext* context, const FlipSpec* request, FlipResponse* response) override; + grpc::Status GetFaults(grpc::ServerContext* context, const FlipNameRequest* request, + FlipListResponse* response) override; + grpc::Status RemoveFault(grpc::ServerContext*, const FlipRemoveRequest* request, + FlipRemoveResponse* response) override; +}; +} // namespace flip diff --git a/include/sisl/grpc/generic_service.hpp b/include/sisl/grpc/generic_service.hpp new file mode 100644 index 00000000..a401feea --- /dev/null +++ b/include/sisl/grpc/generic_service.hpp @@ -0,0 +1,174 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include "sisl/fds/buffer.hpp" +#include "rpc_call.hpp" + +namespace sisl { + +/** + * Callbacks are registered by a name. The client generic stub uses the method name to call the RPC + * We assume the Request and Response types are grpc::ByteBuffer + * The user is responsible to serialize / deserialize their messages to and from grpc::ByteBuffer + */ + +class GenericRpcStaticInfo : public RpcStaticInfoBase { +public: + GenericRpcStaticInfo(GrpcServer* server, grpc::AsyncGenericService* service) : + m_server{server}, m_generic_service{service} {} + + GrpcServer* m_server; + grpc::AsyncGenericService* m_generic_service; +}; + +class GenericRpcContextBase { +public: + virtual ~GenericRpcContextBase() = default; +}; +using generic_rpc_ctx_ptr = std::unique_ptr< GenericRpcContextBase >; + +class GenericRpcData : public RpcDataAbstract, sisl::ObjLifeCounter< GenericRpcData > { +public: + static RpcDataAbstract* make(GenericRpcStaticInfo* rpc_info, size_t queue_idx) { + return new GenericRpcData(rpc_info, queue_idx); + } + + RpcDataAbstract* create_new() override { return new GenericRpcData(m_rpc_info, m_queue_idx); } + void set_status(grpc::Status& status) { m_retstatus = status; } + + ~GenericRpcData() override { + if (m_request_blob_allocated) { m_request_blob.buf_free(); } + } + + // There is only one generic static rpc data for all rpcs. + size_t get_rpc_idx() const override { return 0; } + + const grpc::ByteBuffer& request() const { return m_request; } + sisl::io_blob& request_blob() { + if (m_request_blob.cbytes() == nullptr) { + grpc::Slice slice; + auto status = m_request.TrySingleSlice(&slice); + if (status.ok()) { + m_request_blob.set_bytes(slice.begin()); + m_request_blob.set_size(slice.size()); + } else if (status.error_code() == grpc::StatusCode::FAILED_PRECONDITION) { + // If the ByteBuffer is not made up of single slice, TrySingleSlice() will fail. + // DumpSingleSlice() should work in those cases but will incur a copy. + if (status = m_request.DumpToSingleSlice(&slice); status.ok()) { + m_request_blob.buf_alloc(slice.size()); + m_request_blob_allocated = true; + std::memcpy(voidptr_cast(m_request_blob.bytes()), c_voidptr_cast(slice.begin()), + slice.size()); + } + } + } + return m_request_blob; + } + + grpc::ByteBuffer& response() { return m_response; } + + void enqueue_call_request(::grpc::ServerCompletionQueue& cq) override { + m_rpc_info->m_generic_service->RequestCall(&m_ctx, &m_stream, &cq, &cq, + static_cast< void* >(m_request_received_tag.ref())); + } + + void send_response() { m_stream.Write(m_response, static_cast< void* >(m_buf_write_tag.ref())); } + + void set_context(generic_rpc_ctx_ptr ctx) { m_rpc_context = std::move(ctx); } + GenericRpcContextBase* get_context() { return m_rpc_context.get(); } + + void set_comp_cb(generic_rpc_completed_cb_t const& comp_cb) { m_comp_cb = comp_cb; } + + GenericRpcData(GenericRpcStaticInfo* rpc_info, size_t queue_idx) : + RpcDataAbstract{queue_idx}, m_rpc_info{rpc_info}, m_stream(&m_ctx) {} + +private: + GenericRpcStaticInfo* m_rpc_info; + grpc::GenericServerAsyncReaderWriter m_stream; + grpc::GenericServerContext m_ctx; + grpc::ByteBuffer m_request; + grpc::ByteBuffer m_response; + sisl::io_blob m_request_blob; + bool m_request_blob_allocated{false}; + grpc::Status m_retstatus{grpc::Status::OK}; + // user can set and retrieve the context. Its life cycle is tied to that of rpc data. + generic_rpc_ctx_ptr m_rpc_context; + // the handler cb can fill in the completion cb if it needs one + generic_rpc_completed_cb_t m_comp_cb{nullptr}; + +private: + bool do_authorization() { + m_retstatus = RPCHelper::do_authorization(m_rpc_info->m_server, &m_ctx); + return m_retstatus.error_code() == grpc::StatusCode::OK; + } + + RpcDataAbstract* on_request_received(bool ok) { + bool in_shutdown = RPCHelper::has_server_shutdown(m_rpc_info->m_server); + + if (ok) { + if (!do_authorization()) { + m_stream.Finish(m_retstatus, static_cast< void* >(m_request_completed_tag.ref())); + } else { + m_stream.Read(&m_request, static_cast< void* >(m_buf_read_tag.ref())); + } + } + + return in_shutdown ? nullptr : create_new(); + } + + RpcDataAbstract* on_buf_read(bool) { + auto this_rpc_data = boost::intrusive_ptr< GenericRpcData >{this}; + // take a ref before the handler cb is called. + // unref is called in send_response which is handled by us (in case of sync calls) + // or by the handler (for async calls) + ref(); + if (RPCHelper::run_generic_handler_cb(m_rpc_info->m_server, m_ctx.method(), this_rpc_data)) { send_response(); } + return nullptr; + } + + RpcDataAbstract* on_buf_write(bool) { + m_stream.Finish(m_retstatus, static_cast< void* >(m_request_completed_tag.ref())); + unref(); + return nullptr; + } + + RpcDataAbstract* on_request_completed(bool) { + auto this_rpc_data = boost::intrusive_ptr< GenericRpcData >{this}; + if (m_comp_cb) { m_comp_cb(this_rpc_data); } + return nullptr; + } + + struct RpcTagImpl : public RpcTag { + using callback_type = RpcDataAbstract* (GenericRpcData::*)(bool); + RpcTagImpl(GenericRpcData* rpc, callback_type cb) : RpcTag{rpc}, m_callback{cb} {} + + RpcDataAbstract* do_process(bool ok) override { + return (static_cast< GenericRpcData* >(m_rpc_data)->*m_callback)(ok); + } + + callback_type m_callback; + }; + + // Used as void* completion markers from grpc to indicate different events of interest for a + // Call. + RpcTagImpl m_request_received_tag{this, &GenericRpcData::on_request_received}; + RpcTagImpl m_buf_read_tag{this, &GenericRpcData::on_buf_read}; + RpcTagImpl m_buf_write_tag{this, &GenericRpcData::on_buf_write}; + RpcTagImpl m_request_completed_tag{this, &GenericRpcData::on_request_completed}; +}; + +} // namespace sisl diff --git a/include/sisl/grpc/rpc_call.hpp b/include/sisl/grpc/rpc_call.hpp new file mode 100644 index 00000000..5407265e --- /dev/null +++ b/include/sisl/grpc/rpc_call.hpp @@ -0,0 +1,429 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include "rpc_common.hpp" + +SISL_LOGGING_DECL(grpc_server) + +#define RPC_SERVER_LOG(level, msg, ...) \ + LOG##level##MOD_FMT(grpc_server, ([&](fmt::memory_buffer& buf, const char* __m, auto&&... args) -> bool { \ + fmt::vformat_to(fmt::appender{buf}, std::string_view{"[{}:{}] [RPC={} id={}] "}, \ + fmt::make_format_args(file_name(__FILE__), __LINE__, \ + m_rpc_info->m_rpc_name, request_id())); \ + fmt::vformat_to(fmt::appender{buf}, fmt::string_view{__m}, \ + fmt::make_format_args(std::forward< decltype(args) >(args)...)); \ + return true; \ + }), \ + msg, ##__VA_ARGS__); + +namespace sisl { +class RpcDataAbstract : public boost::intrusive_ref_counter< RpcDataAbstract, boost::thread_safe_counter > { +public: + RpcDataAbstract(size_t queue_idx) : + m_queue_idx{queue_idx}, m_request_id(s_glob_request_id.fetch_add(1, std::memory_order_relaxed)) {} + + virtual ~RpcDataAbstract() = default; + virtual size_t get_rpc_idx() const = 0; + + ::grpc::ServerContext& server_context() noexcept { return m_ctx; } + uint64_t request_id() const { return m_request_id; } + bool canceled() const { return m_is_canceled; } + + // enqueues this call to be matched with incoming rpc requests + virtual void enqueue_call_request(::grpc::ServerCompletionQueue& cq) = 0; + + // the grpc queue index on which this request is to be enqueued + size_t const m_queue_idx; + +protected: + // ref counter of this instance + RpcDataAbstract* ref() { + intrusive_ptr_add_ref(this); + return this; + } + void unref() { intrusive_ptr_release(this); } + virtual RpcDataAbstract* create_new() = 0; + friend class RpcTag; + + uint64_t const m_request_id; + grpc::ServerContext m_ctx; + std::atomic_bool m_is_canceled{false}; + static inline std::atomic< uint64_t > s_glob_request_id = 0; +}; + +// Associates a tag in a `::grpc::CompletionQueue` with a callback +// for an incoming RPC. An active Tag owns a reference on the corresponding +// RpcData object. +class RpcTag { +public: + RpcTag(RpcDataAbstract* rpc) : m_rpc_data{rpc} {} + RpcTag* ref() { + m_rpc_data->ref(); + return this; + } + // Calls the callback associated with this tag. + // The callback takes ownership of `this->call_`. + // @return if not null - a replacement of this call for registration with the server; null otherwise + RpcDataAbstract* process(bool ok) { + RpcDataAbstract* ret = do_process(ok); + m_rpc_data->unref(); // undo ref() acquired when tag handed over to grpc. + return ret; + } + +protected: + virtual RpcDataAbstract* do_process(bool ok) = 0; + RpcDataAbstract* const m_rpc_data; // `this` owns one reference. +}; + +class RpcStaticInfoBase { +public: + virtual ~RpcStaticInfoBase() = default; +}; + +template < typename ServiceT, typename ReqT, typename RespT, bool streaming > +class RpcData; + +template < typename ServiceT, typename ReqT, typename RespT > +using AsyncRpcDataPtr = boost::intrusive_ptr< RpcData< ServiceT, ReqT, RespT, false > >; + +template < typename ServiceT, typename ReqT, typename RespT > +using StreamRpcDataPtr = boost::intrusive_ptr< RpcData< ServiceT, ReqT, RespT, true > >; + +#define RPC_DATA_PTR_SPEC boost::intrusive_ptr< RpcData< ServiceT, ReqT, RespT, streaming > > +#define request_call_cb_t \ + std::function< void(typename ServiceT::AsyncService*, ::grpc::ServerContext*, ReqT*, \ + ::grpc::ServerAsyncResponseWriter< RespT >*, ::grpc::CompletionQueue*, \ + ::grpc::ServerCompletionQueue*, void*) > +#define rpc_handler_cb_t std::function< bool(const RPC_DATA_PTR_SPEC& rpc_call) > +#define rpc_completed_cb_t std::function< void(const RPC_DATA_PTR_SPEC& rpc_call) > +#define rpc_call_static_info_t RpcStaticInfo< ServiceT, ReqT, RespT, streaming > +#define rpc_sync_handler_cb_t std::function< ::grpc::Status(const ReqT&, RespT&) > + +// This class represents all static information related to a specific RpcData, so these information does not need to be +// built for every RPC +template < typename ServiceT, typename ReqT, typename RespT, bool streaming = false > +class RpcStaticInfo : public RpcStaticInfoBase { +public: + RpcStaticInfo(GrpcServer* server, typename ServiceT::AsyncService& svc, const request_call_cb_t& call_cb, + const rpc_handler_cb_t& rpc_cb, const rpc_completed_cb_t& comp_cb, size_t idx, + const std::string& name) : + m_server{server}, + m_svc{svc}, + m_req_call_cb{call_cb}, + m_handler_cb{rpc_cb}, + m_comp_cb{comp_cb}, + m_rpc_idx{idx}, + m_rpc_name{name} {} + + GrpcServer* m_server; + typename ServiceT::AsyncService& m_svc; + request_call_cb_t m_req_call_cb; + rpc_handler_cb_t m_handler_cb; + rpc_completed_cb_t m_comp_cb; + size_t m_rpc_idx; + std::string m_rpc_name; +}; + +/** + * This class represents an incoming request and its associated context + * Template argument 'streaming' should be understood as server streaming. If we later want + * client/bidirectional streaming then we can restructure this code + */ +template < typename ServiceT, typename ReqT, typename RespT, bool streaming = false > +class RpcData : public RpcDataAbstract, sisl::ObjLifeCounter< RpcData< ServiceT, ReqT, RespT, streaming > > { +public: + static RpcDataAbstract* make(rpc_call_static_info_t* rpc_info, size_t queue_idx) { + return new RpcData(rpc_info, queue_idx); + } + + RpcDataAbstract* create_new() override { return new RpcData(m_rpc_info, m_queue_idx); } + ~RpcData() override = default; + + const ReqT& request() const { return *m_request; } + + template < bool mode = streaming > + std::enable_if_t< !mode, RespT& > response() { + return *m_response; + } + + void set_status(grpc::Status status) { m_retstatus = status; } + + // invoked by the application completion flow when the response payload `m_response` is formed + //@param is_last - true to indicate that this is the last chunk in a streaming response (where + // applicable) + // NOTE: this function MUST `unref()` this call + template < bool mode = streaming > + std::enable_if_t< !mode, void > send_response([[maybe_unused]] bool is_last = true) { + do_non_streaming_send(); + } + + /** + * @param response, the response should own by m_arena_resp + * @param is_last + * @return return false, when we can't send send_response anymore. + * The reasons includes: + * 1. last streaming response has sent + * + * 2. the rpc call has canceled. + * 3. ok == false in ResponseSent + * 4. e.t.c + * Note: We must call send_response with is_last = true once even when the call return false at + * last time to indicate use will not hold the RpcData anymore. + */ + template < bool mode = streaming > + std::enable_if_t< mode, bool > send_response(std::unique_ptr< RespT > response, bool is_last) { + std::lock_guard< std::mutex > lock{m_streaming_mutex}; + if (is_last && !m_last) { + m_last = true; + // ses comment in _start_request_processing + unref(); + } + if (m_streaming_disable_enqueue) { return false; } + if (m_last) { m_streaming_disable_enqueue = true; } + + RPC_SERVER_LOG(DEBUG, "ENQUEUE STREAMING RESPONSE, is_last={}", is_last); + RPC_SERVER_LOG(TRACE, "resp. payload={}", response->DebugString()); + + m_pending_streaming_responses.push(std::move(response)); + do_streaming_send_if_needed(); + return !m_streaming_disable_enqueue; + } + + ::grpc::string get_peer_info() { return m_ctx.peer(); } + std::string get_client_req_context() { + /*if (m_client_req_context.empty()) { + std::string* client_id_str = google::protobuf::Arena::Create< std::string >( + &m_arena_req, m_ctx.peer() + "_" + std::to_string(request_id())); + m_client_req_context = grpc::string_ref(*client_id_str); + } + return m_client_req_context; */ + return fmt::format("{}_{}", m_ctx.peer(), request_id()); + } + size_t get_rpc_idx() const { return m_rpc_info->m_rpc_idx; } + + RpcData(rpc_call_static_info_t* rpc_info, size_t queue_idx) : + RpcDataAbstract{queue_idx}, + m_rpc_info{rpc_info}, + m_request{google::protobuf::Arena::CreateMessage< ReqT >(&m_arena_req)}, + m_response{google::protobuf::Arena::CreateMessage< RespT >(&m_arena_resp)}, + // m_rpc_context{google::protobuf::Arena::Create< context_t >(&m_arena_req, *this)}, + m_responder(&m_ctx), + m_streaming_responder(&m_ctx) {} + +private: + bool do_authorization() { + m_retstatus = RPCHelper::do_authorization(m_rpc_info->m_server, &m_ctx); + return m_retstatus.error_code() == grpc::StatusCode::OK; + } + + // The implementation of this method should dispatch the request for processing by calling + // do_start_request_processing One reference on `this` is transferred to the callee, and the + // callee is responsible for releasing it (typically via `RpcData::send_response(..)`). + // + // `ok` is true if the request was received is a "regular event", otherwise false. + // @return a new instance of the same class for enqueueing as a replacement of this call + RpcDataAbstract* on_request_received(bool ok) { + bool in_shutdown = RPCHelper::has_server_shutdown(m_rpc_info->m_server); + RPC_SERVER_LOG(TRACE, "request received with is_ok={} is_shutdown={}", ok, in_shutdown); + + if (ok) { + ref(); // we now own one ref since we are starting the processing + + RPC_SERVER_LOG(DEBUG, "Received client_req_context={}, from peer={}", get_client_req_context(), + get_peer_info()); + RPC_SERVER_LOG(TRACE, "req. payload={}", request().DebugString()); + + // Autherization + if (!do_authorization()) { + if constexpr (streaming) { + std::lock_guard< std::mutex > lock{m_streaming_mutex}; + do_streaming_send_if_needed(); + } else { + send_response(); + } + } else { + if constexpr (streaming) { + // In no-streaming mode, we call ref() to inc the ref count for keep the RpcData live + // before users finish their work and send responses in RequestReceived. + // But in streaming mode, The time user finishes their work may be different to + // the time grpc finsihes the grpc call. E.g.: + // 1) The user queues the last streaming resposne. At that time. We can't unref the RpcData and + // must do it after it sends all responses. + // 2) The user queues a no-last streaming response, then RpcData find the call was canceled. + // We can't unref the call, because users don't know it, they will send next responses. + // So instead of using only one ref in no-streaming mode. We use two ref to make lifecyle clear: + // 1) first one in RequestReceived and unref after grpc call finished. + // 2) second one in here and unref after called send_response with is_last = true; + ref(); + } + if (m_rpc_info->m_handler_cb(RPC_DATA_PTR_SPEC{this})) { send_response(); } + } + } + + return in_shutdown ? nullptr : create_new(); + } + + // This method will be called in response to one of `m_responder.Finish*` flavours + RpcDataAbstract* on_response_sent(bool ok) { + RPC_SERVER_LOG(TRACE, "response sent with is_ok={}", ok); + + if constexpr (streaming) { + if (ok) { + std::lock_guard< std::mutex > lock{m_streaming_mutex}; + m_write_pending = false; + do_streaming_send_if_needed(); + } else { + m_streaming_disable_enqueue = true; + // The ResponseSent can be triggered by Write, WriteAndFinish and Finish. + // Only when it triggered by Write, we should call unref() + if (!m_streaming_disable_send) { unref(); } + } + } + return nullptr; + } + + // This method will be called either (i) when the server is notified that the request has been canceled, or (ii) + // when the request completes normally. The implementation should distinguish these cases by querying the + // grpc::ServerContext associated with the request. + RpcDataAbstract* on_request_completed(bool ok) { + RPC_SERVER_LOG(TRACE, "request completed with is_ok={}", ok); + if (m_ctx.IsCancelled()) { + m_is_canceled.store(true, std::memory_order_release); + RPC_SERVER_LOG(DEBUG, "request is CANCELLED by the caller"); + } + if (m_rpc_info->m_comp_cb) { m_rpc_info->m_comp_cb(RPC_DATA_PTR_SPEC{this}); } + return nullptr; + } + + void enqueue_call_request(::grpc::ServerCompletionQueue& cq) override { + RPC_SERVER_LOG(TRACE, "enqueue new call request"); + + if (m_rpc_info->m_comp_cb) { + // Creates a completion queue tag for handling cancellation by the client. + // NOTE: This method must be called before this call is enqueued on a completion queue. + m_ctx.AsyncNotifyWhenDone(m_completed_tag.ref()); + } + + m_rpc_info->m_req_call_cb(&m_rpc_info->m_svc, &m_ctx, m_request, &m_responder, &cq, &cq, + m_request_received_tag.ref()); + } + + // actual sending of the response via grpc + // MUST unref() after send is enqueued + void do_non_streaming_send() { + if (!m_is_canceled.load(std::memory_order_relaxed)) { + RPC_SERVER_LOG(DEBUG, "SENDING RESPONSE"); + RPC_SERVER_LOG(TRACE, "resp. payload={}", m_response->DebugString()); + + if (m_retstatus.ok()) { + m_responder.Finish(*m_response, grpc::Status::OK, m_response_sent_tag.ref()); + } else { + m_responder.FinishWithError(m_retstatus, m_response_sent_tag.ref()); + } + } + unref(); // because we have enqueued response for this call and not longer own it + } + + // MUST be called in streaming mode and under m_streaming_mutex. + void do_streaming_send_if_needed() { + if (m_streaming_disable_send) { return; } + + if (m_is_canceled.load(std::memory_order_relaxed)) { + m_streaming_disable_enqueue = true; + m_streaming_disable_send = true; + unref(); + return; + } + + if (m_write_pending) { return; } + + if (!m_retstatus.ok()) { + m_streaming_responder.Finish(m_retstatus, m_response_sent_tag.ref()); + m_streaming_disable_enqueue = true; + m_streaming_disable_send = true; + unref(); + return; + } + + if (m_pending_streaming_responses.empty()) { return; } + auto response = std::move(m_pending_streaming_responses.front()); + m_pending_streaming_responses.pop(); + if (m_pending_streaming_responses.empty() && m_streaming_disable_enqueue) { + RPC_SERVER_LOG(DEBUG, "SENDING LAST STREAMING RESPONSE"); + RPC_SERVER_LOG(TRACE, "resp. payload={}", m_response->DebugString()); + + m_streaming_responder.WriteAndFinish(*response, grpc::WriteOptions(), grpc::Status::OK, + m_response_sent_tag.ref()); + m_streaming_disable_send = true; + unref(); + } else { + RPC_SERVER_LOG(DEBUG, "SENDING STREAMING RESPONSE"); + RPC_SERVER_LOG(TRACE, "resp. payload={}", m_response->DebugString()); + m_streaming_responder.Write(*response, grpc::WriteOptions(), m_response_sent_tag.ref()); + m_write_pending = true; + } + } + +private: + rpc_call_static_info_t* m_rpc_info; + ::google::protobuf::Arena m_arena_req, m_arena_resp; + ReqT* const m_request; + RespT* const m_response; + + // this field is used when there is a high level grpc-level request error + grpc::Status m_retstatus{grpc::Status::OK}; + + grpc::ServerAsyncResponseWriter< RespT > m_responder; + grpc::ServerAsyncWriter< RespT > m_streaming_responder; + + std::mutex m_streaming_mutex; + bool m_last{false}; + bool m_write_pending{false}; + bool m_streaming_disable_enqueue{false}; + bool m_streaming_disable_send{false}; + std::queue< std::unique_ptr< RespT > > m_pending_streaming_responses; + + // implements abstract method `_process() by delegating to registered pointer to member function + struct RpcTagImpl : public RpcTag { + using callback_type = RpcDataAbstract* (RpcData::*)(bool ok); + RpcTagImpl(RpcData* rpc, callback_type cb) : RpcTag{rpc}, m_callback{cb} {} + + RpcDataAbstract* do_process(bool ok) override { return (static_cast< RpcData* >(m_rpc_data)->*m_callback)(ok); } + + callback_type m_callback; + }; + + // Used as void* completion markers from grpc to indicate different events of interest for a + // Call. + RpcTagImpl m_request_received_tag{this, &RpcData::on_request_received}; + RpcTagImpl m_response_sent_tag{this, &RpcData::on_response_sent}; + RpcTagImpl m_completed_tag{this, &RpcData::on_request_completed}; +}; + +} // namespace sisl diff --git a/include/sisl/grpc/rpc_client.hpp b/include/sisl/grpc/rpc_client.hpp new file mode 100644 index 00000000..5b54190d --- /dev/null +++ b/include/sisl/grpc/rpc_client.hpp @@ -0,0 +1,456 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace grpc { +inline auto format_as(StatusCode s) { return fmt::underlying(s); } +} // namespace grpc + +namespace sisl { + +/** + * A interface for handling gRPC async response + */ +class ClientRpcDataAbstract : private boost::noncopyable { +public: + virtual ~ClientRpcDataAbstract() = default; + virtual void handle_response(bool ok = true) = 0; +}; + +template < typename ReqT, typename RespT > +class ClientRpcData; + +template < typename ReqT, typename RespT > +using rpc_comp_cb_t = std::function< void(ClientRpcData< ReqT, RespT >& cd) >; + +template < typename ReqT > +using req_builder_cb_t = std::function< void(ReqT&) >; + +template < typename RespT > +using unary_callback_t = std::function< void(RespT&, ::grpc::Status& status) >; + +template < typename ReqT, typename RespT > +class ClientRpcDataCallback; + +template < typename ReqT, typename RespT > +class ClientRpcDataFuture; + +template < typename T > +using Result = folly::Expected< T, ::grpc::Status >; + +template < typename T > +using AsyncResult = folly::SemiFuture< Result< T > >; + +using GenericClientRpcData = ClientRpcData< grpc::ByteBuffer, grpc::ByteBuffer >; +using generic_rpc_comp_cb_t = rpc_comp_cb_t< grpc::ByteBuffer, grpc::ByteBuffer >; +using generic_req_builder_cb_t = req_builder_cb_t< grpc::ByteBuffer >; +using generic_unary_callback_t = unary_callback_t< grpc::ByteBuffer >; +using GenericClientRpcDataCallback = ClientRpcDataCallback< grpc::ByteBuffer, grpc::ByteBuffer >; +using GenericClientRpcDataFuture = ClientRpcDataFuture< grpc::ByteBuffer, grpc::ByteBuffer >; +using generic_result_t = Result< grpc::ByteBuffer >; +using generic_async_result_t = AsyncResult< grpc::ByteBuffer >; + +/** + * The specialized 'ClientRpcDataInternal' per gRPC call, + * Derive from this class to create Rpc Data that can hold + * the response handler function or a promise + */ +template < typename ReqT, typename RespT > +class ClientRpcDataInternal : public ClientRpcDataAbstract { +public: + using ResponseReaderPtr = std::unique_ptr<::grpc::ClientAsyncResponseReaderInterface< RespT > >; + using GenericResponseReaderPtr = std::unique_ptr< grpc::GenericClientAsyncResponseReader >; + + /* Allow GrpcAsyncClient and its inner classes to use + * ClientCallData. + */ + friend class GrpcAsyncClient; + + ClientRpcDataInternal() = default; + virtual ~ClientRpcDataInternal() = default; + + // TODO: support time in any time unit -- lhuang8 + void set_deadline(uint32_t seconds) { + std::chrono::system_clock::time_point deadline = + std::chrono::system_clock::now() + std::chrono::seconds(seconds); + m_context.set_deadline(deadline); + } + + ResponseReaderPtr& responder_reader() { return m_resp_reader_ptr; } + ::grpc::Status& status() { return m_status; } + RespT& reply() { return m_reply; } + ::grpc::ClientContext& context() { return m_context; } + + virtual void handle_response(bool ok = true) = 0; + + void add_metadata(const std::string& meta_key, const std::string& meta_value) { + m_context.AddMetadata(meta_key, meta_value); + } + + RespT m_reply; + ::grpc::ClientContext m_context; + ::grpc::Status m_status; + ResponseReaderPtr m_resp_reader_ptr; + GenericResponseReaderPtr m_generic_resp_reader_ptr; +}; + +/** + * callback version of ClientRpcDataInternal + */ +template < typename ReqT, typename RespT > +class ClientRpcDataCallback : public ClientRpcDataInternal< ReqT, RespT > { +public: + ClientRpcDataCallback(const unary_callback_t< RespT >& cb) : m_cb{cb} {} + + virtual void handle_response([[maybe_unused]] bool ok = true) override { + // For unary call, ok is always true, `status_` will indicate error if there are any. + if (m_cb) { m_cb(this->m_reply, this->m_status); } + } + + unary_callback_t< RespT > m_cb; +}; + +/** + * futures version of ClientRpcDataInternal + */ +template < typename ReqT, typename RespT > +class ClientRpcDataFuture : public ClientRpcDataInternal< ReqT, RespT > { +public: + ClientRpcDataFuture(folly::Promise< Result< RespT > >&& promise) : m_promise{std::move(promise)} {} + + virtual void handle_response([[maybe_unused]] bool ok = true) override { + // For unary call, ok is always true, `status_` will indicate error if there are any. + if (this->m_status.ok()) { + m_promise.setValue(this->m_reply); + } else { + m_promise.setValue(folly::makeUnexpected(this->m_status)); + } + } + + folly::Promise< Result< RespT > > m_promise; +}; + +template < typename ReqT, typename RespT > +class ClientRpcData : public ClientRpcDataInternal< ReqT, RespT > { +public: + ClientRpcData(const rpc_comp_cb_t< ReqT, RespT >& comp_cb) : m_comp_cb{comp_cb} {} + virtual ~ClientRpcData() = default; + + virtual void handle_response([[maybe_unused]] bool ok = true) override { + // For unary call, ok is always true, `status_` will indicate error if there are any. + m_comp_cb(*this); + // Caller could delete this pointer and thus don't acccess anything after this. + } + + const ReqT& req() { return m_req; } + + rpc_comp_cb_t< ReqT, RespT > m_comp_cb; + ReqT m_req; +}; + +/** + * A GrpcBaseClient takes care of establish a channel to grpc + * server. The channel can be used by any number of grpc + * generated stubs. + * + */ +class GrpcBaseClient { +protected: + const std::string m_server_addr; + const std::string m_target_domain; + const std::string m_ssl_cert; + + std::shared_ptr<::grpc::ChannelInterface > m_channel; + std::shared_ptr< sisl::GrpcTokenClient > m_token_client; + +public: + GrpcBaseClient(const std::string& server_addr, const std::string& target_domain = "", + const std::string& ssl_cert = ""); + GrpcBaseClient(const std::string& server_addr, const std::shared_ptr< sisl::GrpcTokenClient >& token_client, + const std::string& target_domain = "", const std::string& ssl_cert = ""); + virtual ~GrpcBaseClient() = default; + virtual bool is_connection_ready() const; + virtual void init(); + +private: + virtual bool load_ssl_cert(const std::string& ssl_cert, std::string& content); +}; + +class GrpcSyncClient : public GrpcBaseClient { +public: + using GrpcBaseClient::GrpcBaseClient; + + template < typename ServiceT > + std::unique_ptr< typename ServiceT::StubInterface > MakeStub() { + return ServiceT::NewStub(m_channel); + } +}; + +ENUM(ClientState, uint8_t, VOID, INIT, RUNNING, SHUTTING_DOWN, TERMINATED) + +/** + * One GrpcBaseClient can have multiple stub + * + * The gRPC client worker, it owns a CompletionQueue and one or more threads, + * it's only used for handling asynchronous responses. + * + * The CompletionQueue is used to send asynchronous request, then the + * response will be handled on worker threads. + * + */ +class GrpcAsyncClientWorker final { +public: + using UPtr = std::unique_ptr< GrpcAsyncClientWorker >; + + GrpcAsyncClientWorker() = default; + ~GrpcAsyncClientWorker(); + + void run(uint32_t num_threads); + + ::grpc::CompletionQueue& cq() { return m_cq; } + + static void create_worker(const std::string& name, int num_threads); + static GrpcAsyncClientWorker* get_worker(const std::string& name); + + /** + * Must be called explicitly before program exit if any worker created. + */ + static void shutdown_all(); + +private: + /* + * Shutdown CompletionQueue and threads. + * + * For now, workers can only by shutdown by + * GrpcAsyncClientWorker::shutdown_all(). + */ + void shutdown(); + void client_loop(); + +private: + static std::mutex s_workers_mtx; + static std::unordered_map< std::string, GrpcAsyncClientWorker::UPtr > s_workers; + + ClientState m_state{ClientState::INIT}; + ::grpc::CompletionQueue m_cq; + std::vector< std::thread > m_threads; +}; + +// common request id header +static std::string const request_id_header{"request_id"}; + +class GrpcAsyncClient : public GrpcBaseClient { +public: + template < typename ServiceT > + using StubPtr = std::unique_ptr< typename ServiceT::StubInterface >; + + GrpcAsyncClient(const std::string& server_addr, const std::shared_ptr< sisl::GrpcTokenClient > token_client, + const std::string& target_domain = "", const std::string& ssl_cert = "") : + GrpcBaseClient(server_addr, token_client, target_domain, ssl_cert) {} + + GrpcAsyncClient(const std::string& server_addr, const std::string& target_domain = "", + const std::string& ssl_cert = "") : + GrpcAsyncClient(server_addr, nullptr, target_domain, ssl_cert) {} + + virtual ~GrpcAsyncClient() {} + + /** + * AsyncStub is a wrapper of generated service stub. + * + * An AsyncStub is created with a GrpcAsyncClientWorker, all responses + * of grpc async calls made on it will be handled on the + * GrpcAsyncClientWorker's threads. + * + * Please use GrpcAsyncClient::make_stub() to create AsyncStub. + * + */ + template < typename ServiceT > + struct AsyncStub { + using UPtr = std::unique_ptr< AsyncStub >; + + AsyncStub(StubPtr< ServiceT > stub, GrpcAsyncClientWorker* worker, + std::shared_ptr< sisl::GrpcTokenClient > token_client) : + m_stub(std::move(stub)), m_worker(worker), m_token_client(token_client) {} + + using stub_t = typename ServiceT::StubInterface; + + /* unary call helper */ + template < typename RespT > + using unary_call_return_t = std::unique_ptr<::grpc::ClientAsyncResponseReaderInterface< RespT > >; + + template < typename ReqT, typename RespT > + using unary_call_t = unary_call_return_t< RespT > (stub_t::*)(::grpc::ClientContext*, const ReqT&, + ::grpc::CompletionQueue*); + + template < typename ReqT, typename RespT > + void prepare_and_send_unary(ClientRpcDataInternal< ReqT, RespT >* data, const ReqT& request, + const unary_call_t< ReqT, RespT >& method, uint32_t deadline, + const std::vector< std::pair< std::string, std::string > >& metadata) { + data->set_deadline(deadline); + for (auto const& [key, value] : metadata) { + data->add_metadata(key, value); + } + if (m_token_client) { + data->add_metadata(m_token_client->get_auth_header_key(), m_token_client->get_token()); + } + // Note that async unary RPCs don't post a CQ tag in call + data->m_resp_reader_ptr = (m_stub.get()->*method)(&data->m_context, request, cq()); + // CQ tag posted here + data->m_resp_reader_ptr->Finish(&data->reply(), &data->status(), (void*)data); + } + + // using unary_callback_t = std::function< void(RespT&, ::grpc::Status& status) >; + + /** + * Make a unary call. + * + * @param request - a request of this unary call. + * @param call - a pointer to a member function in grpc service stub + * which used to make an aync call. If service name is + * "EchoService" and an unary rpc is defined as: + * ` rpc Echo (EchoRequest) returns (EchoReply) {}` + * then the member function used here should be: + * `EchoService::StubInterface::AsyncEcho`. + * @param callback - the response handler function, which will be + * called after response received asynchronously or call failed(which + * would happen if the channel is either permanently broken or + * transiently broken, or call timeout). + * The callback function must check if `::grpc::Status` argument is + * OK before handling the response. If call failed, `::grpc::Status` + * indicates the error code and error message. + * @param deadline - deadline in seconds + * @param metadata - key value pair of the metadata to be sent with the request + * + */ + template < typename ReqT, typename RespT > + void call_unary(const ReqT& request, const unary_call_t< ReqT, RespT >& method, + const unary_callback_t< RespT >& callback, uint32_t deadline, + const std::vector< std::pair< std::string, std::string > >& metadata) { + auto data = new ClientRpcDataCallback< ReqT, RespT >(callback); + prepare_and_send_unary(data, request, method, deadline, metadata); + } + + template < typename ReqT, typename RespT > + void call_unary(const ReqT& request, const unary_call_t< ReqT, RespT >& method, + const unary_callback_t< RespT >& callback, uint32_t deadline) { + call_unary(request, method, callback, deadline, {}); + } + + template < typename ReqT, typename RespT > + void call_rpc(const req_builder_cb_t< ReqT >& builder_cb, const unary_call_t< ReqT, RespT >& method, + const rpc_comp_cb_t< ReqT, RespT >& done_cb, uint32_t deadline) { + auto cd = new ClientRpcData< ReqT, RespT >(done_cb); + builder_cb(cd->m_req); + prepare_and_send_unary(cd, cd->m_req, method, deadline, {}); + } + + // Futures version of call_unary + template < typename ReqT, typename RespT > + AsyncResult< RespT > call_unary(const ReqT& request, const unary_call_t< ReqT, RespT >& method, + uint32_t deadline, + const std::vector< std::pair< std::string, std::string > >& metadata) { + auto [p, sf] = folly::makePromiseContract< Result< RespT > >(); + auto data = new ClientRpcDataFuture< ReqT, RespT >(std::move(p)); + prepare_and_send_unary(data, request, method, deadline, metadata); + return std::move(sf); + } + + template < typename ReqT, typename RespT > + AsyncResult< RespT > call_unary(const ReqT& request, const unary_call_t< ReqT, RespT >& method, + uint32_t deadline) { + return call_unary(request, method, deadline, {}); + } + + StubPtr< ServiceT > m_stub; + GrpcAsyncClientWorker* m_worker; + std::shared_ptr< sisl::GrpcTokenClient > m_token_client; + + const StubPtr< ServiceT >& stub() { return m_stub; } + + ::grpc::CompletionQueue* cq() { return &m_worker->cq(); } + }; + + /** + * GenericAsyncStub is a wrapper of the grpc::GenericStub which + * provides the interface to call generic methods by name. + * We assume the Request and Response types are grpc::ByteBuffer. + * + * Please use GrpcAsyncClient::make_generic_stub() to create GenericAsyncStub. + */ + + struct GenericAsyncStub { + GenericAsyncStub(std::unique_ptr< grpc::GenericStub > stub, GrpcAsyncClientWorker* worker, + std::shared_ptr< sisl::GrpcTokenClient > token_client) : + m_generic_stub(std::move(stub)), m_worker(worker), m_token_client(token_client) {} + + void prepare_and_send_unary_generic(ClientRpcDataInternal< grpc::ByteBuffer, grpc::ByteBuffer >* data, + const grpc::ByteBuffer& request, const std::string& method, uint32_t deadline); + + void call_unary(const grpc::ByteBuffer& request, const std::string& method, + const generic_unary_callback_t& callback, uint32_t deadline); + + void call_rpc(const generic_req_builder_cb_t& builder_cb, const std::string& method, + const generic_rpc_comp_cb_t& done_cb, uint32_t deadline); + + // futures version of call_unary + generic_async_result_t call_unary(const grpc::ByteBuffer& request, const std::string& method, + uint32_t deadline); + + std::unique_ptr< grpc::GenericStub > m_generic_stub; + GrpcAsyncClientWorker* m_worker; + std::shared_ptr< sisl::GrpcTokenClient > m_token_client; + + grpc::CompletionQueue* cq() { return &m_worker->cq(); } + }; + + template < typename T, typename... Ts > + static auto make(Ts&&... params) { + return std::make_unique< T >(std::forward< Ts >(params)...); + } + + template < typename ServiceT > + auto make_stub(const std::string& worker) { + auto w = GrpcAsyncClientWorker::get_worker(worker); + if (w == nullptr) { throw std::runtime_error("worker thread not available"); } + + return std::make_unique< AsyncStub< ServiceT > >(ServiceT::NewStub(m_channel), w, m_token_client); + } + + std::unique_ptr< GenericAsyncStub > make_generic_stub(const std::string& worker); +}; + +} // namespace sisl diff --git a/include/sisl/grpc/rpc_common.hpp b/include/sisl/grpc/rpc_common.hpp new file mode 100644 index 00000000..c5fc09be --- /dev/null +++ b/include/sisl/grpc/rpc_common.hpp @@ -0,0 +1,30 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +namespace sisl { +class GrpcServer; +class GenericRpcData; + +using generic_rpc_handler_cb_t = std::function< bool(boost::intrusive_ptr< GenericRpcData >&) >; +using generic_rpc_completed_cb_t = std::function< void(boost::intrusive_ptr< GenericRpcData >&) >; + +struct RPCHelper { + static bool has_server_shutdown(const GrpcServer* server); + static bool run_generic_handler_cb(GrpcServer* server, const std::string& method, + boost::intrusive_ptr< GenericRpcData >& rpc_data); + static grpc::Status do_authorization(const GrpcServer* server, const grpc::ServerContext* srv_ctx); +}; +} // namespace sisl::grpc diff --git a/include/sisl/grpc/rpc_server.hpp b/include/sisl/grpc/rpc_server.hpp new file mode 100644 index 00000000..e23d1109 --- /dev/null +++ b/include/sisl/grpc/rpc_server.hpp @@ -0,0 +1,146 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include "rpc_call.hpp" + +namespace sisl { +class GenericRpcData; +class GenericRpcStaticInfo; + +using rpc_thread_start_cb_t = std::function< void(uint32_t) >; + +ENUM(ServerState, uint8_t, VOID, INITED, RUNNING, SHUTTING_DOWN, TERMINATED) + +class GrpcServer : private boost::noncopyable { + friend class RPCHelper; + +public: + GrpcServer(const std::string& listen_addr, uint32_t threads, const std::string& ssl_key, + const std::string& ssl_cert); + GrpcServer(const std::string& listen_addr, uint32_t threads, const std::string& ssl_key, + const std::string& ssl_cert, const std::shared_ptr< sisl::GrpcTokenVerifier >& auth_mgr); + virtual ~GrpcServer(); + + /** + * Create a new GrpcServer instance and initialize it. + */ + static GrpcServer* make(const std::string& listen_addr, uint32_t threads = 1, const std::string& ssl_key = "", + const std::string& ssl_cert = ""); + static GrpcServer* make(const std::string& listen_addr, const std::shared_ptr< sisl::GrpcTokenVerifier >& auth_mgr, + uint32_t threads = 1, const std::string& ssl_key = "", const std::string& ssl_cert = ""); + + void run(const rpc_thread_start_cb_t& thread_start_cb = nullptr); + void shutdown(); + bool is_terminated() const { return m_state.load(std::memory_order_acquire) == ServerState::TERMINATED; } + + template < typename ServiceT > + bool register_async_service() { + DEBUG_ASSERT_EQ(ServerState::INITED, m_state, "register service in non-INITED state"); + + auto name = ServiceT::service_full_name(); + if (m_services.find(name) != m_services.end()) { + LOGMSG_ASSERT(false, "Duplicate register async service"); + return false; + } + + auto svc = new typename ServiceT::AsyncService(); + m_builder.RegisterService(svc); + m_services.insert({name, svc}); + + return true; + } + + template < typename ServiceT, typename ReqT, typename RespT, bool streaming = false > + bool register_rpc(const std::string& name, const request_call_cb_t& request_call_cb, + const rpc_handler_cb_t& rpc_handler, const rpc_completed_cb_t& done_handler = nullptr) { + DEBUG_ASSERT_EQ(ServerState::RUNNING, m_state, "register service in non-INITED state"); + + auto it = m_services.find(ServiceT::service_full_name()); + if (it == m_services.end()) { + LOGMSG_ASSERT(false, "RPC registration attempted before service is registered"); + return false; + } + + auto svc = static_cast< typename ServiceT::AsyncService* >(it->second); + + size_t rpc_idx; + { + std::unique_lock lg(m_rpc_registry_mtx); + rpc_idx = m_rpc_registry.size(); + m_rpc_registry.emplace_back(new RpcStaticInfo< ServiceT, ReqT, RespT, false >( + this, *svc, request_call_cb, rpc_handler, done_handler, rpc_idx, name)); + + // Register one call per cq. + for (auto i = 0u; i < m_cqs.size(); ++i) { + auto rpc_call = RpcData< ServiceT, ReqT, RespT, false >::make( + (rpc_call_static_info_t*)m_rpc_registry[rpc_idx].get(), i); + rpc_call->enqueue_call_request(*m_cqs[i]); + } + } + + return true; + } + + template < typename ServiceT, typename ReqT, typename RespT, bool streaming = false > + bool register_sync_rpc(const std::string& name, const request_call_cb_t& request_call_cb, + const rpc_sync_handler_cb_t& handler) { + return register_rpc(name, request_call_cb, [handler](const RPC_DATA_PTR_SPEC& rpc_data) -> bool { + rpc_data->set_status(handler(rpc_data->request(), rpc_data->response())); + return true; + }); + } + + bool is_auth_enabled() const; + grpc::Status auth_verify(grpc::ServerContext const* srv_ctx) const; + + // generic service methods + bool run_generic_handler_cb(const std::string& rpc_name, boost::intrusive_ptr< GenericRpcData >& rpc_data); + void run_generic_completion_cb(const std::string& rpc_name, boost::intrusive_ptr< GenericRpcData >& rpc_data); + bool register_async_generic_service(); + bool register_generic_rpc(const std::string& name, const generic_rpc_handler_cb_t& rpc_handler); + +private: + void handle_rpcs(uint32_t thread_num, const rpc_thread_start_cb_t& thread_start_cb); + +private: + std::atomic< ServerState > m_state{ServerState::VOID}; + uint32_t m_num_threads{0}; + ::grpc::ServerBuilder m_builder; + + std::unique_ptr< ::grpc::Server > m_server; + std::vector< std::shared_ptr< std::thread > > m_threads; + std::vector< std::unique_ptr< ::grpc::ServerCompletionQueue > > m_cqs; + + std::unordered_map< const char*, ::grpc::Service* > m_services; + std::mutex m_rpc_registry_mtx; + std::vector< std::unique_ptr< RpcStaticInfoBase > > m_rpc_registry; + std::shared_ptr< sisl::GrpcTokenVerifier > m_auth_mgr; + std::unique_ptr< grpc::AsyncGenericService > m_generic_service; + std::unique_ptr< GenericRpcStaticInfo > m_generic_rpc_static_info; + bool m_generic_service_registered{false}; + std::unordered_map< std::string, generic_rpc_handler_cb_t > m_generic_rpc_registry; + std::shared_mutex m_generic_rpc_registry_mtx; +}; +} // namespace sisl::grpc diff --git a/src/logging/logging.h b/include/sisl/logging/logging.h similarity index 96% rename from src/logging/logging.h rename to include/sisl/logging/logging.h index 7593c368..ebcf7a43 100644 --- a/src/logging/logging.h +++ b/include/sisl/logging/logging.h @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd, Harihara Kadayam + * Author/Developer(s): Brian Szmyd, Harihara Kadayam * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,17 +28,16 @@ #include #include #include +#include #include #include +#include #include #include #include #include -#include -#include #include -#include #include #include // NOTE: There is an ordering dependecy on this header and fmt headers below #include @@ -244,7 +243,7 @@ constexpr const char* file_name(const char* const str) { return str_slant(str) ? #define _ABORT_OR_DUMP(is_log_assert) \ assert(0); \ if (is_log_assert) { \ - if (sisl::logging::is_crash_handler_installed()) { sisl::logging::log_stack_trace(false); } \ + if (sisl::logging::is_crash_handler_installed()) { raise(SIGUSR3); } \ } else { \ abort(); \ } @@ -265,7 +264,7 @@ constexpr const char* file_name(const char* const str) { return str_slant(str) ? * LOGMSG_ASSERT: If condition is not met: Logs the message with stack trace, aborts in debug build only. * DEBUG_ASSERT: No-op in release build, for debug build, if condition is not met, logs the message and aborts */ -//#if __cplusplus > 201703L +// #if __cplusplus > 201703L #if 0 #define _GENERIC_ASSERT(is_log_assert, cond, formatter, msg, ...) \ [[unlikely]] if (!(cond)) { _LOG_AND_ASSERT_FMT(is_log_assert, formatter, msg, ##__VA_ARGS__); } @@ -375,6 +374,20 @@ static constexpr uint32_t max_stacktrace_size() { return static_cast< uint32_t > #define SIGUSR4 SIGUSR2 #endif +class LoggerThreadContext; + +class LoggerThreadRegistry { +public: + std::mutex m_logger_thread_mutex; + std::unordered_set< LoggerThreadContext* > m_logger_thread_set; + +public: + void add_logger_thread(LoggerThreadContext* ctx); + void remove_logger_thread(LoggerThreadContext* ctx); + + static std::shared_ptr< LoggerThreadRegistry > instance(); +}; + class LoggerThreadContext { public: LoggerThreadContext(const LoggerThreadContext&) = delete; @@ -385,16 +398,10 @@ class LoggerThreadContext { static LoggerThreadContext& instance(); - static void add_logger_thread(LoggerThreadContext* const ctx); - - static void remove_logger_thread(LoggerThreadContext* const ctx); - - static std::mutex s_logger_thread_mutex; - static std::unordered_set< LoggerThreadContext* > s_logger_thread_set; - std::shared_ptr< spdlog::logger > m_logger; std::shared_ptr< spdlog::logger > m_critical_logger; pthread_t m_thread_id; + std::shared_ptr< LoggerThreadRegistry > m_logger_thread_registry; // Take reference to avoid singleton destruction private: LoggerThreadContext(); @@ -431,7 +438,7 @@ MODLEVELDEC(_, _, base) #define MODLEVELDEF(r, l, module) \ extern "C" { \ - spdlog::level::level_enum BOOST_PP_CAT(module_level_, module){l}; \ + __attribute__((visibility("default"))) spdlog::level::level_enum BOOST_PP_CAT(module_level_, module){l}; \ } #define MOD_LEVEL_STRING(r, _, module) BOOST_PP_STRINGIZE(module), @@ -439,9 +446,10 @@ MODLEVELDEC(_, _, base) #define SISL_LOGGING_DECL(...) \ BOOST_PP_SEQ_FOR_EACH(MODLEVELDEC, spdlog::level::level_enum::off, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) +#define SISL_LOGGING_DEF(...) \ + BOOST_PP_SEQ_FOR_EACH(MODLEVELDEF, spdlog::level::level_enum::err, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) + #define SISL_LOGGING_INIT(...) \ - BOOST_PP_SEQ_FOR_EACH(MODLEVELDEF, spdlog::level::level_enum::info, \ - BOOST_PP_TUPLE_TO_SEQ(BOOST_PP_VARIADIC_TO_TUPLE(__VA_ARGS__))) \ sisl::logging::InitModules s_init_enabled_mods{ \ BOOST_PP_SEQ_FOR_EACH(MOD_LEVEL_STRING, , BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))}; @@ -473,6 +481,7 @@ extern bool is_crash_handler_installed(); extern bool restore_signal_handler(const SignalType sig_num); extern bool restore_signal_handlers(); extern bool send_thread_signal(const pthread_t thr, const SignalType sig_num); +extern std::filesystem::path get_base_dir(); template < typename... Args > std::string format_log_msg(const char* const msg, Args&&... args) { diff --git a/src/metrics/histogram_buckets.hpp b/include/sisl/metrics/histogram_buckets.hpp similarity index 100% rename from src/metrics/histogram_buckets.hpp rename to include/sisl/metrics/histogram_buckets.hpp diff --git a/src/metrics/metrics.hpp b/include/sisl/metrics/metrics.hpp similarity index 99% rename from src/metrics/metrics.hpp rename to include/sisl/metrics/metrics.hpp index 2768d70a..e3c01f9b 100644 --- a/src/metrics/metrics.hpp +++ b/include/sisl/metrics/metrics.hpp @@ -32,9 +32,9 @@ #include #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include #include "metrics_atomic.hpp" #include "metrics_group_impl.hpp" #include "metrics_rcu.hpp" diff --git a/src/metrics/metrics_atomic.hpp b/include/sisl/metrics/metrics_atomic.hpp similarity index 100% rename from src/metrics/metrics_atomic.hpp rename to include/sisl/metrics/metrics_atomic.hpp diff --git a/src/metrics/metrics_group_impl.hpp b/include/sisl/metrics/metrics_group_impl.hpp similarity index 99% rename from src/metrics/metrics_group_impl.hpp rename to include/sisl/metrics/metrics_group_impl.hpp index 8f159a95..7af5f661 100644 --- a/src/metrics/metrics_group_impl.hpp +++ b/include/sisl/metrics/metrics_group_impl.hpp @@ -30,9 +30,10 @@ #include +#include + #include "histogram_buckets.hpp" #include "prometheus_reporter.hpp" -#include "utility/thread_buffer.hpp" namespace sisl { using on_gather_cb_t = std::function< void(void) >; diff --git a/src/metrics/metrics_rcu.hpp b/include/sisl/metrics/metrics_rcu.hpp similarity index 98% rename from src/metrics/metrics_rcu.hpp rename to include/sisl/metrics/metrics_rcu.hpp index 03c887a3..54092c63 100644 --- a/src/metrics/metrics_rcu.hpp +++ b/include/sisl/metrics/metrics_rcu.hpp @@ -16,13 +16,15 @@ *********************************************************************************/ #pragma once -#include "histogram_buckets.hpp" #include #include #include #include + +#include + +#include "histogram_buckets.hpp" #include "metrics_tlocal.hpp" -#include "wisr/wisr_framework.hpp" namespace sisl { using WisrBufferMetrics = diff --git a/src/metrics/metrics_tlocal.hpp b/include/sisl/metrics/metrics_tlocal.hpp similarity index 100% rename from src/metrics/metrics_tlocal.hpp rename to include/sisl/metrics/metrics_tlocal.hpp diff --git a/src/metrics/prometheus_reporter.hpp b/include/sisl/metrics/prometheus_reporter.hpp similarity index 98% rename from src/metrics/prometheus_reporter.hpp rename to include/sisl/metrics/prometheus_reporter.hpp index 051302b6..a51bddb4 100644 --- a/src/metrics/prometheus_reporter.hpp +++ b/include/sisl/metrics/prometheus_reporter.hpp @@ -33,7 +33,7 @@ #include #pragma GCC diagnostic pop -#include "logging/logging.h" +#include namespace sisl { @@ -77,7 +77,10 @@ class PrometheusReportHistogram : public ReportHistogram { // Since histogram doesn't have reset facility (PR is yet to be accepted in the main repo), // we are doing a placement new to reconstruct the entire object to force to call its constructor. This // way we don't need to register histogram again to family. + using namespace prometheus; + bucket_values.resize(m_bkt_boundaries.size() + 1); + m_histogram.~Histogram(); prometheus::Histogram* inplace_hist = new ((void*)&m_histogram) prometheus::Histogram(m_bkt_boundaries); inplace_hist->ObserveMultiple(bucket_values, sum); } diff --git a/src/metrics/reporter.hpp b/include/sisl/metrics/reporter.hpp similarity index 100% rename from src/metrics/reporter.hpp rename to include/sisl/metrics/reporter.hpp diff --git a/src/options/options.h b/include/sisl/options/options.h similarity index 99% rename from src/options/options.h rename to include/sisl/options/options.h index 14078512..687fb24d 100644 --- a/src/options/options.h +++ b/include/sisl/options/options.h @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/settings/settings.hpp b/include/sisl/settings/settings.hpp similarity index 91% rename from src/settings/settings.hpp rename to include/sisl/settings/settings.hpp index ff6f64d9..01be4257 100644 --- a/src/settings/settings.hpp +++ b/include/sisl/settings/settings.hpp @@ -29,10 +29,9 @@ #include -#include "logging/logging.h" -#include "options/options.h" - -#include "utility/urcu_helper.hpp" +#include +#include +#include #define SETTINGS_INIT(schema_type, schema_name) \ extern unsigned char schema_name##_fbs[]; \ @@ -123,8 +122,7 @@ static bool diff(const reflection::Schema* schema, const reflection::Object* sch break; } - case reflection::BaseType::Array: - case reflection::BaseType::Union: { + default: { // Please do not use unions or arrays in settings. It's crazy! // LOG_ASSERT(false) << "reflection::BaseType::Union type in settings is not supported"; break; @@ -210,8 +208,7 @@ static bool diff_vector(const reflection::Schema* schema, const reflection::Fiel break; } - case reflection::BaseType::Array: - case reflection::BaseType::Union: { + default: { // Please do not use unions or arrays in settings. It's crazy! // LOG_ASSERT(false) << "reflection::BaseType::Union type in settings is not supported"; break; @@ -235,12 +232,13 @@ class SettingsFactoryBase : public boost::noncopyable { class SettingsFactoryRegistry { public: - static SettingsFactoryRegistry& instance() { - static SettingsFactoryRegistry _inst; + static SettingsFactoryRegistry& instance(const std::string& path = "", + const std::vector< std::string >& override_cfgs = {}) { + static SettingsFactoryRegistry _inst{path, override_cfgs}; return _inst; } - SettingsFactoryRegistry(); + SettingsFactoryRegistry(const std::string& path = "", const std::vector< std::string >& override_cfgs = {}); void register_factory(const std::string& s, SettingsFactoryBase* f); void unregister_factory(const std::string& s); @@ -250,6 +248,7 @@ class SettingsFactoryRegistry { private: mutable std::shared_mutex m_mtx; + std::string m_config_path; std::unordered_map< std::string, SettingsFactoryBase* > m_factories; std::unordered_map< std::string, nlohmann::json > m_override_cfgs; }; @@ -302,14 +301,17 @@ class SettingsFactory : public sisl::SettingsFactoryBase { parser.opts.strict_json = true; parser.opts.output_default_scalars_in_json = true; - if (!parser.Parse(m_raw_schema.c_str())) { return; } + if (!parser.Parse(m_raw_schema.c_str())) { + LOGERROR("Error in parsing schema file to save"); + return; + } parser.builder_.Finish( SettingsT::TableType::Pack(parser.builder_, m_rcu_data.get_node()->get().get(), nullptr)); std::string fname = filepath; boost::replace_all(fname, ".json", ""); - if (!GenerateTextFile(parser, "", fname)) { return; } + if (GenTextFile(parser, "", fname) == nullptr) { LOGERROR("Error in Saving json to file"); } } const std::string& get_current_settings() const { return m_current_settings; } @@ -322,12 +324,15 @@ class SettingsFactory : public sisl::SettingsFactoryBase { parser.opts.strict_json = true; parser.opts.output_default_scalars_in_json = true; - if (!parser.Parse(m_raw_schema.c_str())) { return "Error parsing flatbuffer settings schema"; } + if (!parser.Parse(m_raw_schema.c_str())) { + LOGERROR("Error parsing flatbuffer settings schema"); + return json; + } parser.builder_.Finish( SettingsT::TableType::Pack(parser.builder_, m_rcu_data.get_node()->get().get(), nullptr)); - if (!GenerateText(parser, parser.builder_.GetBufferPointer(), &json)) { - return "Error generating json from flatbuffer"; + if (GenText(parser, parser.builder_.GetBufferPointer(), &json) == nullptr) { + LOGERROR("Error generating json from flatbuffer"); } return json; } @@ -335,9 +340,10 @@ class SettingsFactory : public sisl::SettingsFactoryBase { private: void load(const std::string& config, bool is_config_file) { try { - auto new_settings = parse_config(config, is_config_file); + SettingsT new_settings; + parse_config(config, is_config_file, new_settings); // post_process(true, &new_settings); - m_rcu_data.make_and_exchange(new_settings); + m_rcu_data.make_and_exchange(std::move(new_settings)); } catch (std::exception& e) { throw std::runtime_error(fmt::format("Exception reading config {} (errmsg = {})", (is_config_file ? config : " in json"), e.what())); @@ -346,7 +352,8 @@ class SettingsFactory : public sisl::SettingsFactoryBase { bool reload(const std::string& config, bool is_config_file) { try { - auto new_settings = parse_config(config, is_config_file /* is_config_file */); + SettingsT new_settings; + parse_config(config, is_config_file /* is_config_file */, new_settings); /* post_process may reconfigure some settings, therefore this has to be called before taking diff */ // post_process(false, &new_settings); @@ -354,7 +361,7 @@ class SettingsFactory : public sisl::SettingsFactoryBase { m_current_settings = ""; /* getSettings will return empty briefly before exiting */ return true; } else { - m_rcu_data.make_and_exchange(new_settings); + m_rcu_data.make_and_exchange(std::move(new_settings)); } } catch (std::exception& e) { LOGERROR("Exception reading config {} (errmsg = {})", (is_config_file ? config : " in json"), e.what()); @@ -362,7 +369,7 @@ class SettingsFactory : public sisl::SettingsFactoryBase { return false; } - SettingsT parse_config(const std::string& config, bool is_file) { + void parse_config(const std::string& config, bool is_file, SettingsT& out_settings) { std::string json_config_str; if (is_file) { if (!flatbuffers::LoadFile(config.c_str(), false, &json_config_str)) { @@ -392,11 +399,8 @@ class SettingsFactory : public sisl::SettingsFactoryBase { /* parsing succeeded, update current settings string */ m_current_settings = std::move(json_config_str); - SettingsT settings; flatbuffers::GetRoot< typename SettingsT::TableType >(parser.builder_.GetBufferPointer()) - ->UnPackTo(&settings, nullptr); - - return settings; + ->UnPackTo(&out_settings, nullptr); } bool check_restart_needed(const SettingsT* new_settings, const std::shared_ptr< SettingsT > current_settings) { @@ -466,7 +470,7 @@ class SettingsFactory : public sisl::SettingsFactoryBase { #define WITH_SETTINGS_THIS_CAP1(var, cap1, ...) with_settings([ this, cap1 ](auto& var) __VA_ARGS__) #define WITH_SETTINGS_THIS_CAP2(var, cap1, cap2, ...) with_settings([ this, cap1, cap2 ](auto& var) __VA_ARGS__) -//#define SETTINGS_FACTORY(SType) ::sisl::SettingsFactory< SType##T >::instance() +// #define SETTINGS_FACTORY(SType) ::sisl::SettingsFactory< SType##T >::instance() /* * SETTINGS(var) invokes user supplied lamdba passing it a safe pointer to an instance of settings object @@ -486,5 +490,5 @@ class SettingsFactory : public sisl::SettingsFactoryBase { #define SETTINGS_THIS_CAP2(sname, var, cap1, cap2, ...) \ SETTINGS_FACTORY(sname).WITH_SETTINGS_THIS_CAP2(var, cap1, cap2, __VA_ARGS__) -//#define SETTINGS_VALUE(SType, path_expr) SETTINGS_FACTORY(SType).WITH_SETTINGS_VALUE(path_expr) +// #define SETTINGS_VALUE(SType, path_expr) SETTINGS_FACTORY(SType).WITH_SETTINGS_VALUE(path_expr) #define SETTINGS_VALUE(sname, path_expr) SETTINGS_FACTORY(sname).WITH_SETTINGS_VALUE(path_expr) diff --git a/include/sisl/sobject/sobject.hpp b/include/sisl/sobject/sobject.hpp new file mode 100644 index 00000000..c6495390 --- /dev/null +++ b/include/sisl/sobject/sobject.hpp @@ -0,0 +1,113 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace sisl { + +typedef struct status_request { + nlohmann::json json; + bool do_recurse{false}; + int verbose_level = 0; + std::string obj_type; + std::string obj_name; + std::vector< std::string > obj_path; + int batch_size = 10; + std::string next_cursor; +} status_request; + +typedef struct status_response { + nlohmann::json json; +} status_response; + +using status_callback_type = std::function< status_response(const status_request&) >; +class sobject; +class sobject_manager; +using sobject_ptr = std::shared_ptr< sobject >; + +[[maybe_unused]] static status_response status_error(std::string error_str) { + status_response response; + response.json["error"] = error_str; + return response; +} + +// Similar to sysfs kobject, sobject is a lightweight utility to create relationships +// between different classes and modules. This can be used to get or change the state of a class +// and all its children. Modules/subsystems which register their callbacks to be +// whenever a get status is called from the root or directly. Object is uniquely identified by its name. +class sobject { +public: + sobject(sobject_manager* mgr, const std::string& obj_type, const std::string& obj_name, status_callback_type cb) : + m_mgr(mgr), m_type(obj_type), m_name(obj_name), m_status_cb(std::move(cb)) {} + + static sobject_ptr create(sobject_manager* mgr, const std::string& obj_type, const std::string& obj_name, + status_callback_type cb) { + return std::make_shared< sobject >(mgr, obj_type, obj_name, std::move(cb)); + } + + // Every subsystem add to the json object using update(). + status_response run_callback(const status_request& request) const; + sobject_ptr get_child(const std::string& name); + void add_child(const sobject_ptr child); + void add_child_type(const std::string& child_type); + + std::string name() const { return m_name; } + std::string type() const { return m_type; } + +private: + sobject_manager* m_mgr; + std::string m_type; + std::string m_name; + std::shared_mutex m_mtx; + status_callback_type m_status_cb; + // Keep a graph of child nodes. Mapping from name to child status object. + std::map< std::string, sobject_ptr > m_children; + friend class sobject_manager; +}; + +class sobject_manager { +private: +public: + sobject_ptr create_object(const std::string& type, const std::string& name, status_callback_type cb); + status_response get_status(const status_request& request); + + status_response get_child_type_status( const status_request& request); + status_response get_object_by_path(const status_request& request); + status_response get_object_status(const std::string& name, const status_request& request); + status_response get_objects(const status_request& request); + status_response get_object_types(const std::string& type); + void add_object_type(const std::string& parent_type, const std::string& child_type); + +private: + // Mapping from object name to object metadata. Object names are required + // to be unique. + std::map< std::string, sobject_ptr, std::less<> > m_object_store; + // Mapping from parent type to set of all children type to display the schema. + std::map< std::string, std::set< std::string > > m_object_types; + std::shared_mutex m_mtx; +}; + +} // namespace sisl diff --git a/src/utility/atomic_counter.hpp b/include/sisl/utility/atomic_counter.hpp similarity index 100% rename from src/utility/atomic_counter.hpp rename to include/sisl/utility/atomic_counter.hpp diff --git a/src/utility/enum.hpp b/include/sisl/utility/enum.hpp similarity index 69% rename from src/utility/enum.hpp rename to include/sisl/utility/enum.hpp index bcc16e16..05896b12 100644 --- a/src/utility/enum.hpp +++ b/include/sisl/utility/enum.hpp @@ -27,6 +27,8 @@ #include #include +#include + template < typename EnumType > class EnumSupportBase { public: @@ -109,11 +111,16 @@ class EnumSupportBase { }; #define VENUM(EnumName, Underlying, ...) ENUM(EnumName, Underlying, __VA_ARGS__) +#define ENUM(EnumName, Underlying, ...) BASE_ENUM(EnumName, EnumName, Underlying, __VA_ARGS__) +#define SCOPED_ENUM_DEF(Scope, EnumName, Underlying, ...) BASE_ENUM(Scope::EnumName, EnumName, Underlying, __VA_ARGS__) +#define SCOPED_ENUM_DECL(EnumName, Underlying) \ + enum class EnumName : Underlying; \ + struct EnumName##Support; -#define ENUM(EnumName, Underlying, ...) \ - enum class EnumName : Underlying { __VA_ARGS__ }; \ +#define BASE_ENUM(FQEnumName, EnumName, Underlying, ...) \ + enum class FQEnumName : Underlying { __VA_ARGS__ }; \ \ - struct EnumName##Support : EnumSupportBase< EnumName > { \ + struct FQEnumName##Support : EnumSupportBase< EnumName > { \ typedef EnumName enum_type; \ typedef std::underlying_type_t< enum_type > underlying_type; \ EnumName##Support(const std::string tokens) : EnumSupportBase< enum_type >{tokens} {}; \ @@ -127,37 +134,45 @@ class EnumSupportBase { return s_instance; \ }; \ }; \ - [[nodiscard]] inline EnumName##Support::enum_type operator|(const EnumName##Support::enum_type a, \ - const EnumName##Support::enum_type b) { \ - return static_cast< EnumName##Support::enum_type >(static_cast< EnumName##Support::underlying_type >(a) | \ - static_cast< EnumName##Support::underlying_type >(b)); \ + [[nodiscard]] inline auto format_as(FQEnumName##Support::enum_type e) { \ + return FQEnumName##Support::instance().get_name(e); \ + } \ + [[nodiscard]] inline FQEnumName##Support::enum_type operator|(const FQEnumName##Support::enum_type a, \ + const FQEnumName##Support::enum_type b) { \ + return static_cast< FQEnumName##Support::enum_type >(static_cast< FQEnumName##Support::underlying_type >(a) | \ + static_cast< FQEnumName##Support::underlying_type >(b)); \ } \ - [[nodiscard]] inline EnumName##Support::enum_type operator&(const EnumName##Support::enum_type a, \ - const EnumName##Support::enum_type b) { \ - return static_cast< EnumName##Support::enum_type >(static_cast< EnumName##Support::underlying_type >(a) & \ - static_cast< EnumName##Support::underlying_type >(b)); \ + [[nodiscard]] inline FQEnumName##Support::enum_type operator&(const FQEnumName##Support::enum_type a, \ + const FQEnumName##Support::enum_type b) { \ + return static_cast< FQEnumName##Support::enum_type >(static_cast< FQEnumName##Support::underlying_type >(a) & \ + static_cast< FQEnumName##Support::underlying_type >(b)); \ } \ - [[maybe_unused]] inline EnumName##Support::enum_type operator|=(EnumName##Support::enum_type& a, \ - const EnumName##Support::enum_type b) { \ - return a = static_cast< EnumName##Support::enum_type >(static_cast< EnumName##Support::underlying_type >(a) | \ - static_cast< EnumName##Support::underlying_type >(b)); \ + [[maybe_unused]] inline FQEnumName##Support::enum_type operator|=(FQEnumName##Support::enum_type& a, \ + const FQEnumName##Support::enum_type b) { \ + return a = static_cast< FQEnumName##Support::enum_type >( \ + static_cast< FQEnumName##Support::underlying_type >(a) | \ + static_cast< FQEnumName##Support::underlying_type >(b)); \ } \ - [[maybe_unused]] inline EnumName##Support::enum_type operator&=(EnumName##Support::enum_type& a, \ - const EnumName##Support::enum_type b) { \ - return a = static_cast< EnumName##Support::enum_type >(static_cast< EnumName##Support::underlying_type >(a) & \ - static_cast< EnumName##Support::underlying_type >(b)); \ + [[maybe_unused]] inline FQEnumName##Support::enum_type operator&=(FQEnumName##Support::enum_type& a, \ + const FQEnumName##Support::enum_type b) { \ + return a = static_cast< FQEnumName##Support::enum_type >( \ + static_cast< FQEnumName##Support::underlying_type >(a) & \ + static_cast< FQEnumName##Support::underlying_type >(b)); \ } \ template < typename charT, typename traits > \ std::basic_ostream< charT, traits >& operator<<(std::basic_ostream< charT, traits >& out_stream, \ - const EnumName##Support::enum_type es) { \ + const FQEnumName##Support::enum_type es) { \ std::basic_ostringstream< charT, traits > out_stream_copy{}; \ out_stream_copy.copyfmt(out_stream); \ - out_stream_copy << EnumName##Support::instance().get_name(es); \ + out_stream_copy << fmt::format("{}", es); \ out_stream << out_stream_copy.str(); \ return out_stream; \ } \ - [[nodiscard]] inline const std::string& enum_name(const EnumName##Support::enum_type es) { \ - return EnumName##Support::instance().get_name(es); \ + [[nodiscard]] inline const std::string& enum_name(const FQEnumName##Support::enum_type es) { \ + return FQEnumName##Support::instance().get_name(es); \ + } \ + [[nodiscard]] inline FQEnumName##Support::underlying_type enum_value(const FQEnumName##Support::enum_type es) { \ + return static_cast< FQEnumName##Support::underlying_type >(es); \ } #endif // SISL_ENUM_HPP diff --git a/src/utility/non_null_ptr.hpp b/include/sisl/utility/non_null_ptr.hpp similarity index 94% rename from src/utility/non_null_ptr.hpp rename to include/sisl/utility/non_null_ptr.hpp index 13db2f8c..80bab1a9 100644 --- a/src/utility/non_null_ptr.hpp +++ b/include/sisl/utility/non_null_ptr.hpp @@ -93,7 +93,13 @@ struct embedded_t : public T { explicit operator bool() const noexcept { return true; } - T* release() noexcept { return nullptr; } + T* release() noexcept { + embedded_t* ret = new embedded_t(); + *ret = *this; + return static_cast< T* >(ret); + } + + void reset() noexcept { *this = embedded_t{nullptr}; } }; template < class T > diff --git a/src/utility/obj_life_counter.hpp b/include/sisl/utility/obj_life_counter.hpp similarity index 97% rename from src/utility/obj_life_counter.hpp rename to include/sisl/utility/obj_life_counter.hpp index 8c689967..a37c2891 100644 --- a/src/utility/obj_life_counter.hpp +++ b/include/sisl/utility/obj_life_counter.hpp @@ -27,7 +27,7 @@ #if defined(__linux__) || defined(__APPLE__) #include #endif -#include +#include namespace sisl { @@ -173,9 +173,9 @@ class ObjCounterRegistry { return instance; } - static void register_obj(const char* name, pair_of_atomic_ptrs ptrs) {} + static void register_obj(const char*, pair_of_atomic_ptrs) {} - static void foreach (const std::function< void(const std::string&, int64_t, int64_t) >& closure) {} + static void foreach (const std::function< void(const std::string&, int64_t, int64_t) >&) {} static inline void enable_metrics_reporting() {} }; #endif // _PRERELEASE diff --git a/src/utility/status_factory.hpp b/include/sisl/utility/status_factory.hpp similarity index 96% rename from src/utility/status_factory.hpp rename to include/sisl/utility/status_factory.hpp index 9a59ccc1..e8c8382c 100644 --- a/src/utility/status_factory.hpp +++ b/include/sisl/utility/status_factory.hpp @@ -15,8 +15,8 @@ * *********************************************************************************/ #pragma once -#include -#include "logging/logging.h" +#include "urcu_helper.hpp" +#include namespace sisl { template < typename StatusT > diff --git a/src/utility/thread_buffer.hpp b/include/sisl/utility/thread_buffer.hpp similarity index 96% rename from src/utility/thread_buffer.hpp rename to include/sisl/utility/thread_buffer.hpp index 11ebf1b0..48da6f49 100644 --- a/src/utility/thread_buffer.hpp +++ b/include/sisl/utility/thread_buffer.hpp @@ -34,12 +34,11 @@ #include -#include "fds/flexarray.hpp" -#include "fds/sparse_vector.hpp" -#include "utility/atomic_counter.hpp" - -#include "enum.hpp" -#include "urcu_helper.hpp" +#include +#include +#include +#include +#include namespace sisl { @@ -244,14 +243,10 @@ class ThreadRegistry { // std::vector< thread_state_cb_t > m_registered_notifiers; }; -#define thread_registry ThreadRegistry::instance() - class ThreadLocalContext { public: - ThreadLocalContext() { + ThreadLocalContext() : thread_registry{ThreadRegistry::get_instance_ptr()} { this_thread_num = thread_registry->attach(); - // LOGINFO("Created new ThreadLocalContext with thread_num = {}, my_thread_num = {}", this_thread_num, - // my_thread_num()); } ThreadLocalContext(const ThreadLocalContext&) = delete; ThreadLocalContext(ThreadLocalContext&&) noexcept = delete; @@ -271,8 +266,9 @@ class ThreadLocalContext { } static uint64_t get_context(const uint32_t context_id) { return instance()->user_contexts[context_id]; } +public: static thread_local ThreadLocalContext inst; - + std::shared_ptr< ThreadRegistry > thread_registry; // Take reference to control destruction order uint32_t this_thread_num; std::array< uint64_t, 5 > user_contexts; // To store any user contexts }; @@ -286,7 +282,9 @@ class ThreadBuffer { public: template < class... Args1 > ThreadBuffer(Args1&&... args) : - m_args(std::forward< Args1 >(args)...), m_thread_slots(ThreadRegistry::max_tracked_threads()) { + m_args(std::forward< Args1 >(args)...), + thread_registry{ThreadRegistry::get_instance_ptr()}, + m_thread_slots(ThreadRegistry::max_tracked_threads()) { m_buffers.reserve(ThreadRegistry::max_tracked_threads()); m_notify_idx = thread_registry->register_for_sc_notification( std::bind(&ThreadBuffer::on_thread_state_change, this, std::placeholders::_1, std::placeholders::_2)); @@ -437,10 +435,11 @@ class ThreadBuffer { private: sisl::sparse_vector< std::unique_ptr< T > > m_buffers; std::tuple< Args... > m_args; + std::shared_ptr< ThreadRegistry > thread_registry; // Take reference to control destruction order std::shared_mutex m_expand_mutex; boost::dynamic_bitset<> m_thread_slots; std::vector< std::unique_ptr< T > > m_exited_buffers; // Holds buffers whose threads already exited - uint64_t m_notify_idx = 0; + uint64_t m_notify_idx{0}; }; template < typename T, typename... Args > diff --git a/src/utility/thread_factory.hpp b/include/sisl/utility/thread_factory.hpp similarity index 79% rename from src/utility/thread_factory.hpp rename to include/sisl/utility/thread_factory.hpp index 480921ba..16e5323f 100644 --- a/src/utility/thread_factory.hpp +++ b/include/sisl/utility/thread_factory.hpp @@ -16,10 +16,12 @@ *********************************************************************************/ #pragma once +#include #include #include -#include #include +#include +#include #ifdef _POSIX_THREADS #include @@ -57,17 +59,20 @@ std::unique_ptr< std::thread > make_unique_thread(const std::string name, F&& f, }); } -template < class... Args > -std::thread named_thread(const std::string name, Args&&... args) { - auto t = std::thread(std::forward< Args >(args)...); -#ifdef _POSIX_THREADS -#ifndef __APPLE__ - auto tname = name.substr(0, 15); - auto ret = pthread_setname_np(t.native_handle(), tname.c_str()); - if (ret != 0) { LOGERROR("Set name of thread to {} failed ret={}", tname, ret); } -#endif /* __APPLE__ */ +template < class T > +void name_thread([[maybe_unused]] T& t, std::string const& name) { +#if defined(_POSIX_THREADS) && !defined(__APPLE__) + if (auto ret = pthread_setname_np(t.native_handle(), name.substr(0, 15).c_str()); ret != 0) + LOGERROR("Set name of thread to {} failed ret={}", name, ret); +#else + LOGINFO("No ability to set thread name: {}", name); #endif /* _POSIX_THREADS */ +} +template < class... Args > +auto named_thread(const std::string name, Args&&... args) { + auto t = std::thread(std::forward< Args >(args)...); + name_thread(t, name); return t; } diff --git a/src/utility/urcu_helper.hpp b/include/sisl/utility/urcu_helper.hpp similarity index 100% rename from src/utility/urcu_helper.hpp rename to include/sisl/utility/urcu_helper.hpp diff --git a/src/version.hpp b/include/sisl/version.hpp similarity index 100% rename from src/version.hpp rename to include/sisl/version.hpp diff --git a/src/wisr/wisr_ds.hpp b/include/sisl/wisr/wisr_ds.hpp similarity index 100% rename from src/wisr/wisr_ds.hpp rename to include/sisl/wisr/wisr_ds.hpp diff --git a/src/wisr/wisr_framework.hpp b/include/sisl/wisr/wisr_framework.hpp similarity index 98% rename from src/wisr/wisr_framework.hpp rename to include/sisl/wisr/wisr_framework.hpp index 22991dd3..08919e80 100644 --- a/src/wisr/wisr_framework.hpp +++ b/include/sisl/wisr/wisr_framework.hpp @@ -19,8 +19,8 @@ #include #include -#include "utility/thread_buffer.hpp" -#include "utility/urcu_helper.hpp" +#include +#include namespace sisl { diff --git a/prepare.sh b/prepare.sh new file mode 100755 index 00000000..ec92a7be --- /dev/null +++ b/prepare.sh @@ -0,0 +1,8 @@ +set -eu + +echo -n "Exporting custom recipes..." +echo -n "folly." +conan export 3rd_party/folly folly/nu2.2023.12.18.00@ >/dev/null +echo -n "userpace rcu." +conan export 3rd_party/userspace-rcu userspace-rcu/nu2.0.14.0@ >/dev/null +echo "done." diff --git a/prepare_v2.sh b/prepare_v2.sh new file mode 100755 index 00000000..480965bb --- /dev/null +++ b/prepare_v2.sh @@ -0,0 +1,8 @@ +set -eu + +echo -n "Exporting custom recipes..." +echo -n "folly." +conan export 3rd_party/folly --name folly --version nu2.2023.12.18.00 >/dev/null +echo -n "folly." +conan export 3rd_party/userspace-rcu --name userspace-rcu --version nu2.0.14.0 >/dev/null +echo "done." diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..40408586 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required (VERSION 3.11) + +add_subdirectory (logging) +add_subdirectory (options) +add_subdirectory (version) +add_subdirectory (sobject) + +# These sub-libraries currently do not support MacOS due to dependencies +# on Folly and pistache. It is unknown if Windows is supported... +list(APPEND POSIX_LIBRARIES ) +list(APPEND SISL_DEPS ) + +if(${folly_FOUND}) + if(${userspace-rcu_FOUND}) + add_subdirectory (grpc) + list(APPEND POSIX_LIBRARIES + $ + ) + endif() + add_subdirectory (cache) + add_subdirectory (fds) + add_subdirectory (file_watcher) + add_subdirectory (flip) + add_subdirectory (metrics) + add_subdirectory (settings) + add_subdirectory (utility) + add_subdirectory (wisr) + + list(APPEND POSIX_LIBRARIES + $ + $ + $ + $ + $ + ) + list(APPEND SISL_DEPS + folly::folly + breakpad::breakpad + ) +endif() + +add_library(sisl + ${POSIX_LIBRARIES} + $ + $ + $ + $ + ) + +if (DEFINED MALLOC_IMPL) + if (${MALLOC_IMPL} STREQUAL "tcmalloc") + list(APPEND SISL_DEPS gperftools::gperftools) + endif() +endif() + +target_link_libraries(sisl + ${SISL_DEPS} +) diff --git a/src/async_http/CMakeLists.txt b/src/async_http/CMakeLists.txt deleted file mode 100644 index fb18f2c2..00000000 --- a/src/async_http/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -cmake_minimum_required (VERSION 3.10) - -find_package(FlatBuffers REQUIRED) -find_package(evhtp REQUIRED) - -add_flags("-Wno-unused-parameter -Wno-cast-function-type") - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -set(AUTH_DEPS - sisl - ${COMMON_DEPS} - evhtp::evhtp - cpr::cpr - flatbuffers::flatbuffers - jwt-cpp::jwt-cpp - GTest::gmock - ) - -set(TEST_HTTP_SERVER_SOURCES - tests/test_http_server.cpp - ) -add_executable(test_http_server ${TEST_HTTP_SERVER_SOURCES}) -target_link_libraries(test_http_server ${AUTH_DEPS}) - -add_executable(test_http_server_auth - tests/AuthTest.cpp - ) -target_link_libraries(test_http_server_auth ${AUTH_DEPS}) -add_test(NAME test_http_server_auth COMMAND test_http_server_auth) diff --git a/src/async_http/http_server.hpp b/src/async_http/http_server.hpp deleted file mode 100644 index c6fd4e4a..00000000 --- a/src/async_http/http_server.hpp +++ /dev/null @@ -1,583 +0,0 @@ -/********************************************************************************* - * Modifications Copyright 2017-2019 eBay Inc. - * - * Author/Developer(s): Harihara Kadayam - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - *********************************************************************************/ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __linux__ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#endif - -#include -#include -#include -#include -#include - -#include "auth_manager/auth_manager.hpp" -#include "logging/logging.h" -#include "options/options.h" -#include "utility/obj_life_counter.hpp" -#include "utility/thread_factory.hpp" - -SISL_LOGGING_DECL(httpserver_lmod) - -namespace sisl { -class AuthManager; - -////////////////////// Config Definitions ////////////////////// -struct HttpServerConfig { - bool is_tls_enabled; - std::string tls_cert_path; - std::string tls_key_path; - std::string bind_address; - uint32_t server_port; - uint32_t read_write_timeout_secs; - bool is_auth_enabled; -}; - -////////////////////// Internal Event Definitions ////////////////////// -enum event_type_t { - CALLBACK, -}; -struct HttpEvent { - event_type_t m_event_type; - std::function< void() > m_closure; -}; -typedef std::list< HttpEvent > EventList; - -////////////////////// API CallData Definitions ////////////////////// -struct _http_calldata : public boost::intrusive_ref_counter< _http_calldata >, sisl::ObjLifeCounter< _http_calldata > { -public: - friend class HttpServer; - - _http_calldata(evhtp_request_t* req, void* arg = nullptr) : - m_req{req}, m_completed{false}, m_arg{arg}, m_http_code{EVHTP_RES_OK}, m_content_type{"application/json"} { - m_req->cbarg = this; - } - - void set_response(evhtp_res code, const std::string& msg) { - m_http_code = code; - m_response_msg = msg; - } - - void complete() { m_completed = true; } - bool is_completed() const { return m_completed; } - evhtp_request_t* request() { return m_req; } - void* cookie() { return m_arg; } - -private: - evhtp_request_t* m_req; - bool m_completed; - void* m_arg; - std::string m_response_msg; - evhtp_res m_http_code; - const char* m_content_type; -}; - -typedef boost::intrusive_ptr< _http_calldata > HttpCallData; - -////////////////////// Handler Definitions ////////////////////// -typedef std::function< void(HttpCallData) > HttpRequestHandler; -struct _handler_info { - std::string m_uri; - evhtp_callback_cb m_callback; - void* m_arg; - - _handler_info(const std::string& uri, evhtp_callback_cb cb, void* arg = nullptr) : - m_uri{uri}, m_callback{cb}, m_arg{arg} {} - - bool operator<(const _handler_info& other) const { return m_uri < other.m_uri; } -}; - -template < void (*Handler)(HttpCallData) > -static void _request_handler(evhtp_request_t* req, void* arg) { - const HttpCallData cd{new _http_calldata(req, arg)}; - Handler(cd); -} - -#define handler_info(uri, cb, arg) sisl::_handler_info(uri, sisl::_request_handler< cb >, arg) - -////////////////////// Server Implementation ////////////////////// -class HttpServer { -public: - HttpServer(const HttpServerConfig& cfg, const std::vector< _handler_info >& handlers) : - m_cfg{cfg}, m_handlers{handlers}, m_ev_base{nullptr}, m_htp{nullptr}, m_internal_event{nullptr} {} - - HttpServer(const HttpServerConfig& cfg, const std::vector< _handler_info >& handlers, - const std::shared_ptr< AuthManager > auth_mgr) : - m_cfg{cfg}, - m_handlers{handlers}, - m_ev_base{nullptr}, - m_htp{nullptr}, - m_internal_event{nullptr}, - m_auth_mgr{auth_mgr} {} - - HttpServer(const HttpServerConfig& cfg) : HttpServer{cfg, {}} {} - - virtual ~HttpServer() { - std::lock_guard lock{m_event_mutex}; - while (!m_event_list.empty()) { - auto c{std::move(m_event_list.front())}; - m_event_list.pop_front(); - } - } - - int start() { - try { - if (::evthread_use_pthreads() != 0) { throw std::runtime_error{"evthread_use_pthreads error!"}; } - m_http_thread = sisl::make_unique_thread("httpserver", &HttpServer::_run, this); - } catch (const std::system_error& e) { - LOGERROR("Thread creation failed: {} ", e.what()); - return -1; - } - - { - std::unique_lock< std::mutex > lk{m_running_mutex}; - m_ready_cv.wait(lk, [this] { return m_is_running; }); - } - return 0; - } - - int stop() { - run_in_http_thread([this]() { - LOGINFO("Stopping http server event loop."); - if (::event_base_loopbreak(m_ev_base) != 0) { LOGERROR("Error breaking out of admin server loop: "); } - }); - - /* wait for not running indication */ - LOGINFO("Waiting for http server event loop to be stopped."); - { - std::unique_lock< std::mutex > lk{m_running_mutex}; - m_ready_cv.wait(lk, [this] { return !m_is_running; }); - } - LOGINFO("HTTP server event loop stopped."); - - LOGINFO("Waiting for http server thread to join.."); - if (m_http_thread && m_http_thread->joinable()) { - try { - m_http_thread->join(); - } catch (std::exception& e) { LOGERROR("Http thread join error: {}", e.what()); } - } - LOGINFO("HTTP Server thread joined."); - - return 0; - } - - void register_handler_info(const _handler_info& hinfo) { - ::evhtp_set_cb(m_htp, hinfo.m_uri.c_str(), hinfo.m_callback, hinfo.m_arg); - } - - // Commands for admin/diagnostic purposes - // Holding handles to these commands here - evbase_t* get_base() const { return m_ev_base; } - - void run_in_http_thread(std::function< void() > closure) { - HttpEvent event; - event.m_event_type = event_type_t::CALLBACK; - event.m_closure = std::move(closure); - - { - std::lock_guard< std::mutex > lock{m_event_mutex}; - m_event_list.emplace_back(std::move(event)); - } - - ::event_active(m_internal_event, EV_READ | EV_WRITE, 1); - } - - void respond_OK(HttpCallData cd, evhtp_res http_code, const std::string& msg, - const char* content_type = "application/json") { - cd->m_http_code = http_code; - cd->m_response_msg = msg; - cd->m_content_type = content_type; - respond_OK(cd); - } - - void respond_NOTOK(HttpCallData cd, evhtp_res http_code, const std::string& msg) { - cd->m_http_code = http_code; - cd->m_response_msg = msg; - respond_OK(cd); - } - - void respond_OK(HttpCallData cd) { - if (std::this_thread::get_id() == m_http_thread->get_id()) { - http_OK(cd); - } else { - run_in_http_thread([this, cd]() { http_OK(cd); }); - } - } - - void respond_NOTOK(HttpCallData cd) { - if (std::this_thread::get_id() == m_http_thread->get_id()) { - http_NOTOK(cd); - } else { - run_in_http_thread([this, cd]() { http_NOTOK(cd); }); - } - } - - static evhtp_res to_evhtp_res(const AuthVerifyStatus status) { - evhtp_res ret; - switch (status) { - case AuthVerifyStatus::OK: - ret = EVHTP_RES_OK; - break; - case AuthVerifyStatus::UNAUTH: - ret = EVHTP_RES_UNAUTH; - break; - case AuthVerifyStatus::FORBIDDEN: - ret = EVHTP_RES_FORBIDDEN; - break; - default: - ret = EVHTP_RES_BADREQ; - break; - } - return ret; - } - - /* - * The user of the http_server must add a line to call http_auth_verify at the beginning of all the apis defined. - * The ideal way would be for the server to intercept all incoming api calls and do verification before sending it - * down to the url callback function. No proper way could be found to do this. - * One potential way is to use the hooks (per connection hooks/ per request hooks or per cb hooks) which can be set - * at different points in the life cycle of a req. (like on_headers etc) These hooks from evhtp library do not seem - * to work properly when the hook cb functions return anything other than EVHTP_RES_OK For a perfect implementation - * that avoids users to add http_auth_verify before all the apis they define, we need to either explore evhtp lib - * more or switch to a different server like Pistache. - */ - - evhtp_res http_auth_verify(evhtp_request_t* req, std::string& msg) { - if (!m_cfg.is_auth_enabled) { return EVHTP_RES_OK; } - - const std::string bearer{"Bearer "}; - auto* token{::evhtp_header_find(req->headers_in, "Authorization")}; - if (!token) { - msg = "missing auth token in request header"; - LOGDEBUGMOD(httpserver_lmod, "Processing req={}; {}", static_cast< void* >(req), msg); - return EVHTP_RES_UNAUTH; - } - const std::string token_str{token}; - if (token_str.rfind(bearer, 0) != 0) { - msg = "require bearer token in request header"; - LOGDEBUGMOD(httpserver_lmod, "Processing req={}; {}", static_cast< void* >(req), msg); - return EVHTP_RES_UNAUTH; - } - const auto raw_token{token_str.substr(bearer.length())}; - // verify method is expected to not throw - return to_evhtp_res(m_auth_mgr->verify(raw_token, msg)); - } - -#define request_callback(cb) \ - (evhtp_callback_cb) std::bind(&HttpServer::cb, this, std::placeholders::_1, std::placeholders::_2) -#define error_callback(cb) \ - (evhtp_hook) std::bind(&HttpServer::cb, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) - -protected: - /* ****************** All default connection handlers ****************** */ - static evhtp_res register_connection_handlers(evhtp_connection_t* conn, void* arg) { - evhtp_connection_set_hook(conn, evhtp_hook_on_path, (evhtp_hook)HttpServer::request_on_path_handler, arg); - evhtp_connection_set_hook(conn, evhtp_hook_on_request_fini, (evhtp_hook)HttpServer::request_fini_handler, arg); - evhtp_connection_set_hook(conn, evhtp_hook_on_conn_error, (evhtp_hook)HttpServer::connection_error_callback, - arg); - evhtp_connection_set_hook(conn, evhtp_hook_on_error, (evhtp_hook)HttpServer::request_error_handler, arg); - return EVHTP_RES_OK; - } - - static void default_request_handler(evhtp_request_t* req, void* arg) { - HttpServer* const server{static_cast< HttpServer* >(arg)}; - const HttpCallData cd{new _http_calldata(req, arg)}; - server->respond_NOTOK(cd, EVHTP_RES_BADREQ, "Request can't be matched with any handlers\n"); - } - - static evhtp_res request_on_path_handler(evhtp_request_t* req, void* arg) { - [[maybe_unused]] HttpServer* const server{static_cast< HttpServer* >(arg)}; - - const char* path{""}; - if (req->uri && req->uri->path && req->uri->path->full) { path = req->uri->path->full; } - - LOGDEBUGMOD(httpserver_lmod, "Processing req={} path={}", static_cast< void* >(req), path); - return EVHTP_RES_OK; - } - - static evhtp_res request_fini_handler(evhtp_request_t* req, void* arg) { - [[maybe_unused]] HttpServer* const server{static_cast< HttpServer* >(arg)}; - - const char* path{""}; - if (req->uri && req->uri->path && req->uri->path->full) { path = req->uri->path->full; } - LOGDEBUGMOD(httpserver_lmod, "Finishing req={}, path={}", static_cast< void* >(req), path); - - if (req->cbarg != nullptr) { - _http_calldata* const cd{static_cast< _http_calldata* >(req->cbarg)}; - cd->complete(); - intrusive_ptr_release(cd); - } - return EVHTP_RES_OK; - } - - static void connection_error_callback([[maybe_unused]] evhtp_connection_t* conn, evhtp_error_flags type, - void* arg) { - [[maybe_unused]] HttpServer* const server{static_cast< HttpServer* >(arg)}; - LOGERROR("unhandled connection error of type: {}", type); - } - - static void request_error_handler([[maybe_unused]] evhtp_request_t* req, evhtp_error_flags errtype, void* arg) { - [[maybe_unused]] HttpServer* const server{static_cast< HttpServer* >(arg)}; - LOGERROR("Unhandled request error of type: {}", errtype); - } - -private: - int _run() { - int error{0}; - - m_ev_base = ::event_base_new(); - if (m_ev_base == nullptr) { - LOGERROR("event_base_new() failed!"); - return -1; - } - - m_htp = ::evhtp_new(m_ev_base, nullptr); - if (m_htp == nullptr) { - LOGERROR("evhtp_new() failed!"); - ::event_base_free(m_ev_base); - return -1; - } - - if (m_cfg.is_tls_enabled) { - const auto ssl_config{get_ssl_opts_()}; - if (!ssl_config) { - LOGERROR("get_ssl_opts_ failed!"); - ::evhtp_free(m_htp); - ::event_base_free(m_ev_base); - return -1; - } - - if (::evhtp_ssl_init(m_htp, ssl_config.get()) != 0) { - LOGERROR("evhtp_ssl_init failed!"); - ::evhtp_free(m_htp); - ::event_base_free(m_ev_base); - return -1; - } - } - - struct timeval timeout { - m_cfg.read_write_timeout_secs, 0 - }; - - // For internal events - m_internal_event = ::event_new(m_ev_base, -1, EV_TIMEOUT | EV_READ, &HttpServer::internal_event_handler, this); - if (m_internal_event == nullptr) { - LOGERROR("Adding internal event failed!"); - ::evhtp_free(m_htp); - ::event_base_free(m_ev_base); - return error; - } - ::event_add(m_internal_event, &timeout); - - /* set a callback to set per-connection hooks (via a post_accept cb) */ - ::evhtp_set_post_accept_cb(m_htp, &HttpServer::register_connection_handlers, (void*)this); - - // set read and write timeouts - ::evhtp_set_timeouts(m_htp, &timeout, &timeout); - - // Register all handlers and a default handler - for (auto& handler : m_handlers) { - ::evhtp_set_cb(m_htp, handler.m_uri.c_str(), handler.m_callback, handler.m_arg); - } - ::evhtp_set_gencb(m_htp, (evhtp_callback_cb)default_request_handler, (void*)this); - - // bind a socket - error = ::evhtp_bind_socket(m_htp, m_cfg.bind_address.c_str(), uint16_t(m_cfg.server_port), 128); - if (error != 0) { - // handling socket binding failure - LOGERROR("HTTP listener failed to start at address:port = {}:{} ", m_cfg.bind_address, m_cfg.server_port); - // Free the http resources - ::evhtp_free(m_htp); - ::event_base_free(m_ev_base); - return error; - } - - LOGINFO("HTTP Server started at port: {}", m_cfg.server_port); - - // Notify the caller that we are ready. - { - std::lock_guard< std::mutex > lk{m_running_mutex}; - m_is_running = true; - } - m_ready_cv.notify_one(); - - // start event loop, this will block the thread. - error = ::event_base_loop(m_ev_base, 0); - if (error != 0) { LOGERROR("Error starting Http listener loop"); } - - { - std::lock_guard< std::mutex > lk{m_running_mutex}; - m_is_running = false; - } - m_ready_cv.notify_one(); - - // free the resources - ::evhtp_unbind_socket(m_htp); - - // free pipe event - ::event_free(m_internal_event); - - // free evhtp - ::evhtp_free(m_htp); - - // finally free event base - ::event_base_free(m_ev_base); - - LOGINFO("Exiting http server event loop."); - return error; - } - - void _internal_event_handler(evutil_socket_t, short events) { - std::vector< HttpEvent > events_queue; - { - std::lock_guard lock{m_event_mutex}; - while (!m_event_list.empty()) { - events_queue.emplace_back(std::move(m_event_list.front())); - m_event_list.pop_front(); - } - } - - for (auto& event : events_queue) { - switch (event.m_event_type) { - case event_type_t::CALLBACK: - event.m_closure(); - break; - - default: - LOGERROR("Unknown internal event type {} ", event.m_event_type); - break; - } - } - } - - void http_OK(HttpCallData cd) { - evhtp_request_t* const req{cd->request()}; - - const auto* const conn{::evhtp_request_get_connection(req)}; - if (m_cfg.is_tls_enabled) { ::htp_sslutil_add_xheaders(req->headers_out, conn->ssl, HTP_SSLUTILS_XHDR_ALL); } - if (cd->m_content_type) { - ::evhtp_headers_add_header(req->headers_out, evhtp_header_new("Content-Type", cd->m_content_type, 0, 0)); - } - - std::ostringstream ss; - ss << cd->m_response_msg.size(); - - /* valloc should be 1 because ss.str().c_str() is freed once control goes out of this function */ - ::evhtp_headers_add_header(req->headers_out, evhtp_header_new("Content-Length", ss.str().c_str(), 0, 1)); - ::evbuffer_add(req->buffer_out, cd->m_response_msg.c_str(), cd->m_response_msg.size()); - - // Need to increment the calldata reference since evhtp_send_reply will call finish asyncronously and calldata - // needs to stay relavant till that call. - intrusive_ptr_add_ref(cd.get()); - ::evhtp_send_reply(req, cd->m_http_code); - } - - void http_NOTOK(HttpCallData cd) { - evhtp_request_t* const req{cd->request()}; - - const nlohmann::json json = {{"errorCode", cd->m_http_code}, {"errorDetail", cd->m_response_msg}}; - const std::string json_str{json.dump()}; - ::evhtp_headers_add_header(req->headers_out, ::evhtp_header_new("Content-Type", "application/json", 0, 0)); - - std::ostringstream ss; - ss << json_str.size(); - /* valloc should be 1 because ss.str().c_str() is freed once control goes out of this function */ - ::evhtp_headers_add_header(req->headers_out, ::evhtp_header_new("Content-Length", ss.str().c_str(), 0, 1)); - ::evbuffer_add(req->buffer_out, json_str.c_str(), json_str.size()); - - // Need to increment the calldata reference since evhtp_send_reply will call finish asyncronously and calldata - // needs to stay relavant till that call. - intrusive_ptr_add_ref(cd.get()); - ::evhtp_send_reply(req, cd->m_http_code); - } - - static void internal_event_handler(evutil_socket_t socket, short events, void* user_data) { - HttpServer* server{static_cast< HttpServer* >(user_data)}; - server->_internal_event_handler(socket, events); - } - - std::unique_ptr< evhtp_ssl_cfg_t > get_ssl_opts_() { - struct stat f_stat; - auto ssl_config{std::make_unique< evhtp_ssl_cfg_t >()}; - - ssl_config->ssl_opts = 0; // SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1; - ssl_config->pemfile = (char*)m_cfg.tls_cert_path.c_str(); - ssl_config->privfile = (char*)m_cfg.tls_key_path.c_str(); - - if (ssl_config->pemfile) { - if (::stat(ssl_config->pemfile, &f_stat) != 0) { - LOGERROR("Cannot load SSL cert: {}", ssl_config->pemfile); - return nullptr; - } - } - - if (ssl_config->privfile) { - if (::stat(ssl_config->privfile, &f_stat) != 0) { - LOGERROR("Cannot load SSL key: {}", ssl_config->privfile); - return nullptr; - } - } - - return ssl_config; - } - -private: - HttpServerConfig m_cfg; - std::unique_ptr< std::thread > m_http_thread; - std::vector< _handler_info > m_handlers; - - // Maintaining a list of pipe events because multiple threads could add events at the same time. - // Additions and deletions from this list are protected by m_mutex defined . - std::mutex m_event_mutex; - EventList m_event_list; - - mutable evbase_t* m_ev_base; - evhtp_t* m_htp; - struct event* m_internal_event; - - std::mutex m_running_mutex; - bool m_is_running{false}; - std::condition_variable m_ready_cv; - - std::shared_ptr< AuthManager > m_auth_mgr; -}; - -} // namespace sisl diff --git a/src/async_http/tests/AuthTest.cpp b/src/async_http/tests/AuthTest.cpp deleted file mode 100644 index 449b11ca..00000000 --- a/src/async_http/tests/AuthTest.cpp +++ /dev/null @@ -1,517 +0,0 @@ -/** - * The following test cases are taken from OM. - * https://github.corp.ebay.com/SDS/om_cpp/blob/master/src/tests/unit/Middleware/AuthTest.cpp - **/ - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "auth_manager/trf_client.hpp" -#include "http_server.hpp" - -SISL_LOGGING_INIT(httpserver_lmod) -SISL_OPTIONS_ENABLE(logging) - -namespace sisl::testing { -using namespace ::testing; - -/** - * Load public and private keys. - * Assume the keys(id_rsa.pub and id_rsa) are in the same directory as this file - */ - -static std::string get_cur_file_dir() { - const std::string cur_file_path{__FILE__}; - const auto last_slash_pos{cur_file_path.rfind('/')}; - if (last_slash_pos == std::string::npos) { return ""; } - return std::string{cur_file_path.substr(0, last_slash_pos + 1)}; -} - -static const std::string cur_file_dir{get_cur_file_dir()}; - -static const std::string grant_path = fmt::format("{}/dummy_grant.cg", cur_file_dir); - -static const std::string load_test_data(const std::string& file_name) { - std::ifstream f{fmt::format("{}/{}", cur_file_dir, file_name)}; - std::string buffer{std::istreambuf_iterator< char >{f}, std::istreambuf_iterator< char >{}}; - if (!buffer.empty() && std::isspace(buffer.back())) buffer.pop_back(); - return buffer; -} - -static const std::string rsa_pub_key{load_test_data("id_rsa.pub")}; -static const std::string rsa_priv_key{load_test_data("id_rsa")}; -static const std::string rsa_pub1_key{load_test_data("id_rsa1.pub")}; - -/** - * This will by default construct a valid jwt token, which contains exactly the - * same attributes in heeader and payload claims. In some test cases if we want - * to build a token with some invalid attributes, we must explicitly set those - * attributes. - * - * A trustfabric token: - * Header claims - * alg: RS256 - * kid: 779112af - * typ: JWT - * x5u: https://trustfabric.vip.ebay.com/v2/k/779112af - * - * Payload claims - * iss: trustfabric - * aud: [usersessionauthsvc, protegoreg, fountauth, monstor, ...] - * cluster: 92 - * ns: sds-tess92-19 - * iat: 1610081499 - * exp: 1610083393 - * nbf: 1610081499 - * instances: 10.175.165.15 - * sub: - * uid=sdsapp,networkaddress=10.175.165.15,ou=orchmanager+l=production,o=sdstess9219,dc=tess,dc=ebay,dc=com - * ver: 2 - * vpc: production - */ -struct TestToken { - using token_t = jwt::builder; - - TestToken() : - token{jwt::create() - .set_type("JWT") - .set_algorithm("RS256") - .set_key_id("abc123") - .set_issuer("trustfabric") - .set_header_claim("x5u", jwt::claim(std::string{"http://127.0.0.1:12347/dummy_tf_token"})) - .set_audience(std::set< std::string >{"test-sisl", "protegoreg"}) - .set_issued_at(std::chrono::system_clock::now() - std::chrono::seconds(180)) - .set_not_before(std::chrono::system_clock::now() - std::chrono::seconds(180)) - .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds(180)) - .set_subject("uid=sdsapp,networkaddress=10.175.165.15,ou=orchmanager+l=" - "production,o=testapp,dc=tess,dc=ebay,dc=com") - .set_payload_claim("ver", jwt::claim(std::string{"2"})) - .set_payload_claim("vpc", jwt::claim(std::string{"production"})) - .set_payload_claim("instances", jwt::claim(std::string{"10.175.65.15"}))} {} - - std::string sign_rs256() { return token.sign(jwt::algorithm::rs256(rsa_pub_key, rsa_priv_key, "", "")); } - std::string sign_rs512() { return token.sign(jwt::algorithm::rs512(rsa_pub_key, rsa_priv_key, "", "")); } - token_t& get_token() { return token; } - -private: - token_t token; -}; - -class MockAuthManager : public AuthManager { -public: - using AuthManager::AuthManager; - MOCK_METHOD(std::string, download_key, (const std::string&), (const)); -}; - -class AuthBaseTest : public ::testing::Test { -public: - AuthBaseTest() = default; - AuthBaseTest(const AuthBaseTest&) = delete; - AuthBaseTest& operator=(const AuthBaseTest&) = delete; - AuthBaseTest(AuthBaseTest&&) noexcept = delete; - AuthBaseTest& operator=(AuthBaseTest&&) noexcept = delete; - virtual ~AuthBaseTest() override = default; - - virtual void SetUp() override { - cfg.is_tls_enabled = false; - cfg.bind_address = "127.0.0.1"; - cfg.server_port = 12345; - cfg.read_write_timeout_secs = 10; - } - - virtual void TearDown() override { mock_server->stop(); } - - static void say_hello(HttpCallData cd) { - std::string msg; - if (auto r = pThis(cd)->mock_server->http_auth_verify(cd->request(), msg); r != EVHTP_RES_OK) { - pThis(cd)->mock_server->respond_NOTOK(cd, r, msg); - return; - } - std::cout << "Client is saying hello\n"; - pThis(cd)->mock_server->respond_OK(cd, EVHTP_RES_OK, "Hello client from async_http server\n"); - } - -protected: - HttpServerConfig cfg; - std::unique_ptr< HttpServer > mock_server; - static AuthBaseTest* pThis(HttpCallData cd) { return (AuthBaseTest*)cd->cookie(); } -}; - -class AuthEnableTest : public AuthBaseTest { -public: - AuthEnableTest() = default; - AuthEnableTest(const AuthEnableTest&) = delete; - AuthEnableTest& operator=(const AuthEnableTest&) = delete; - AuthEnableTest(AuthEnableTest&&) noexcept = delete; - AuthEnableTest& operator=(AuthEnableTest&&) noexcept = delete; - virtual ~AuthEnableTest() override = default; - - virtual void SetUp() override { - AuthBaseTest::SetUp(); - load_settings(); - cfg.is_auth_enabled = true; - mock_auth_mgr = std::shared_ptr< MockAuthManager >(new MockAuthManager()); - mock_server = std::unique_ptr< HttpServer >(new HttpServer( - cfg, {handler_info("/api/v1/sayHello", AuthBaseTest::say_hello, (void*)this)}, mock_auth_mgr)); - mock_server->start(); - } - - virtual void TearDown() override { AuthBaseTest::TearDown(); } - - void set_allowed_to_all() { - SECURITY_SETTINGS_FACTORY().modifiable_settings([](auto& s) { s.auth_manager->auth_allowed_apps = "all"; }); - SECURITY_SETTINGS_FACTORY().save(); - } - - static void load_settings() { - SECURITY_SETTINGS_FACTORY().modifiable_settings([](auto& s) { - s.auth_manager->auth_allowed_apps = "app1, testapp, app2"; - s.auth_manager->tf_token_url = "http://127.0.0.1"; - s.auth_manager->leeway = 0; - s.auth_manager->issuer = "trustfabric"; - }); - SECURITY_SETTINGS_FACTORY().save(); - } - -protected: - std::shared_ptr< MockAuthManager > mock_auth_mgr; -}; - -class AuthDisableTest : public AuthBaseTest { -public: - AuthDisableTest() = default; - AuthDisableTest(const AuthDisableTest&) = delete; - AuthDisableTest& operator=(const AuthDisableTest&) = delete; - AuthDisableTest(AuthDisableTest&&) noexcept = delete; - AuthDisableTest& operator=(AuthDisableTest&&) noexcept = delete; - virtual ~AuthDisableTest() override = default; - - virtual void SetUp() { - AuthBaseTest::SetUp(); - cfg.is_auth_enabled = false; - mock_server = std::unique_ptr< HttpServer >( - new HttpServer(cfg, {handler_info("/api/v1/sayHello", AuthBaseTest::say_hello, (void*)this)})); - mock_server->start(); - } - - virtual void TearDown() { AuthBaseTest::TearDown(); } -}; - -// test the TestToken utility, should not raise -TEST(TokenGenerte, sign_and_decode) { - const auto token{TestToken().sign_rs256()}; - const auto verify{jwt::verify().allow_algorithm(jwt::algorithm::rs256(rsa_pub_key)).with_issuer("trustfabric")}; - const auto decoded{jwt::decode(token)}; - verify.verify(decoded); -} - -TEST_F(AuthDisableTest, allow_all_on_disabled_mode) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - const auto resp{cpr::Post(url)}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_all_on_enabled_mode) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - const auto resp{cpr::Post(url)}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(0); -} - -TEST_F(AuthEnableTest, allow_vaid_token) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", TestToken().sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_basic_auth) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(0); - // has basic auth in requester header, we require bearer token - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Basic {}", TestToken().sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_garbage_auth) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(0); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", "Bearer abcdefgh"}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_wrong_algorithm) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - // we currently only support rs256 - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", TestToken().sign_rs512())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(401, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_untrusted_issuer) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - // token is issued by an untrusted issuer, we only trust "trustfabric" - auto token{TestToken()}; - token.get_token().set_issuer("do_not_trust_me"); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", token.sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_untrusted_keyurl) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(0); - // the key url is an untrusted address, we only trust "http://127.0.0.1" - auto token{TestToken()}; - token.get_token().set_header_claim("x5u", jwt::claim(std::string{"http://untrusted.addr/keys/abc123"})); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", token.sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_expired_token) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - // token expired 1 second ago - auto token{TestToken()}; - token.get_token().set_expires_at(std::chrono::system_clock::now() - std::chrono::seconds(1)); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", token.sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_download_key_fail) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Throw(std::runtime_error("download key failed"))); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", TestToken().sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(0); -} - -TEST_F(AuthEnableTest, reject_wrong_key) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub1_key)); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", TestToken().sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_UNAUTHORIZED, resp.status_code); -} - -TEST_F(AuthEnableTest, allow_all_apps) { - set_allowed_to_all(); - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - auto token{TestToken()}; - token.get_token().set_subject("any-prefix,o=dummy_app,dc=tess,dc=ebay,dc=com"); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", token.sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); -} - -TEST_F(AuthEnableTest, reject_unauthorized_app) { - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - // the client application is "myapp", which is not in the allowed list - auto token{TestToken()}; - token.get_token().set_subject("any-prefix,o=myapp,dc=tess,dc=ebay,dc=com"); - const auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", token.sign_rs256())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_FORBIDDEN, resp.status_code); -} - -// Testing trf client -class MockTrfClient : public TrfClient { -public: - using TrfClient::TrfClient; - MOCK_METHOD(void, request_with_grant_token, ()); - void set_token(const std::string& raw_token, const std::string token_type) { - m_access_token = raw_token; - m_token_type = token_type; - m_expiry = std::chrono::system_clock::now() + std::chrono::seconds(2000); - } - // deligate to parent class (run the real method) - - void __request_with_grant_token() { TrfClient::request_with_grant_token(); } - - void set_expiry(std::chrono::system_clock::time_point tp) { m_expiry = tp; } - std::string get_access_token() { return m_access_token; } - std::string get_token_type() { return m_token_type; } -}; - -static void load_trf_settings() { - std::ofstream outfile{grant_path}; - outfile << "dummy cg contents\n"; - outfile.close(); - SECURITY_SETTINGS_FACTORY().modifiable_settings([](auto& s) { - s.trf_client->grant_path = grant_path; - s.trf_client->server = "127.0.0.1:12345/token"; - s.auth_manager->verify = false; - s.auth_manager->leeway = 30; - }); - SECURITY_SETTINGS_FACTORY().save(); -} - -static void remove_grant_path() { std::remove(grant_path.c_str()); } - -// this test will take 10 seconds to run -TEST_F(AuthEnableTest, trf_grant_path_failure) { - load_trf_settings(); - remove_grant_path(); - EXPECT_THROW( - { - try { - TrfClient trf_client; - } catch (const std::runtime_error& e) { - const std::string cmp_string{ - fmt::format("trustfabric client grant path {} does not exist", grant_path)}; - EXPECT_STREQ(e.what(), cmp_string.c_str()); - throw e; - } - }, - std::runtime_error); -} - -TEST_F(AuthEnableTest, trf_allow_valid_token) { - load_trf_settings(); - MockTrfClient mock_trf_client; - const auto raw_token{TestToken().sign_rs256()}; - // mock_trf_client is expected to be called twice - // 1. First time when access_token is empty - // 2. When token is set to be expired - EXPECT_CALL(mock_trf_client, request_with_grant_token()).Times(2); - ON_CALL(mock_trf_client, request_with_grant_token()) - .WillByDefault( - testing::Invoke([&mock_trf_client, &raw_token]() { mock_trf_client.set_token(raw_token, "Bearer"); })); - - const cpr::Url url{"http://127.0.0.1:12345/api/v1/sayHello"}; - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - auto resp{cpr::Post(url, cpr::Header{{"Authorization", fmt::format("Bearer {}", mock_trf_client.get_token())}})}; - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); - - // use the acces_token saved from the previous call - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - resp = cpr::Post(url, cpr::Header{{"Authorization", mock_trf_client.get_typed_token()}}); - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); - - // set token to be expired invoking request_with_grant_token - mock_trf_client.set_expiry(std::chrono::system_clock::now() - std::chrono::seconds(100)); - EXPECT_CALL(*mock_auth_mgr, download_key(_)).Times(1).WillOnce(Return(rsa_pub_key)); - resp = cpr::Post(url, cpr::Header{{"Authorization", mock_trf_client.get_typed_token()}}); - EXPECT_FALSE(resp.error); - EXPECT_EQ(cpr::status::HTTP_OK, resp.status_code); -} - -// Test request_with_grant_token. Setup http server with path /token to return token json -class TrfClientTest : public ::testing::Test { -public: - TrfClientTest() = default; - TrfClientTest(const TrfClientTest&) = delete; - TrfClientTest& operator=(const TrfClientTest&) = delete; - TrfClientTest(TrfClientTest&&) noexcept = delete; - TrfClientTest& operator=(TrfClientTest&&) noexcept = delete; - virtual ~TrfClientTest() override = default; - - virtual void SetUp() override { - cfg.is_tls_enabled = false; - cfg.bind_address = "127.0.0.1"; - cfg.server_port = 12345; - cfg.read_write_timeout_secs = 10; - cfg.is_auth_enabled = false; - mock_server = std::unique_ptr< HttpServer >( - new HttpServer(cfg, {handler_info("/token", TrfClientTest::get_token, this)})); - mock_server->start(); - } - - virtual void TearDown() override { mock_server->stop(); } - - static void get_token(HttpCallData cd) { - std::string msg; - if (const auto r{pThis(cd)->mock_server->http_auth_verify(cd->request(), msg)}; r != EVHTP_RES_OK) { - pThis(cd)->mock_server->respond_NOTOK(cd, r, msg); - return; - } - std::cout << "sending token to client" << std::endl; - pThis(cd)->mock_server->respond_OK(cd, EVHTP_RES_OK, m_token_response); - } - - static void set_token_response(const std::string& raw_token) { - m_token_response = "{\n" - " \"access_token\": \"" + - raw_token + - "\",\n" - " \"token_type\": \"Bearer\",\n" - " \"expires_in\": \"2000\",\n" - " \"refresh_token\": \"dummy_refresh_token\"\n" - "}"; - } - -protected: - HttpServerConfig cfg; - std::unique_ptr< HttpServer > mock_server; - static TrfClientTest* pThis(HttpCallData cd) { return (TrfClientTest*)cd->cookie(); } - static std::string m_token_response; -}; -std::string TrfClientTest::m_token_response; - -TEST_F(TrfClientTest, trf_grant_path_load_failure) { - load_trf_settings(); - MockTrfClient mock_trf_client; - EXPECT_CALL(mock_trf_client, request_with_grant_token()).Times(1); - ON_CALL(mock_trf_client, request_with_grant_token()).WillByDefault(testing::Invoke([&mock_trf_client]() { - mock_trf_client.__request_with_grant_token(); - })); - remove_grant_path(); - EXPECT_THROW( - { - try { - mock_trf_client.get_token(); - } catch (const std::runtime_error& e) { - EXPECT_EQ( - e.what(), - fmt::format("could not load grant from path {}", SECURITY_DYNAMIC_CONFIG(trf_client->grant_path))); - throw e; - } - }, - std::runtime_error); -} - -TEST_F(TrfClientTest, request_with_grant_token) { - load_trf_settings(); - MockTrfClient mock_trf_client; - const auto raw_token{TestToken().sign_rs256()}; - TrfClientTest::set_token_response(raw_token); - EXPECT_CALL(mock_trf_client, request_with_grant_token()).Times(1); - ON_CALL(mock_trf_client, request_with_grant_token()).WillByDefault(testing::Invoke([&mock_trf_client]() { - mock_trf_client.__request_with_grant_token(); - })); - mock_trf_client.get_token(); - EXPECT_EQ(raw_token, mock_trf_client.get_access_token()); - EXPECT_EQ("Bearer", mock_trf_client.get_token_type()); -} - -} // namespace sisl::testing - -using namespace sisl; -using namespace sisl::testing; - -int main(int argc, char* argv[]) { - ::testing::InitGoogleMock(&argc, argv); - SISL_OPTIONS_LOAD(argc, argv, logging) - return RUN_ALL_TESTS(); -} diff --git a/src/async_http/tests/id_rsa b/src/async_http/tests/id_rsa deleted file mode 100644 index 1427e0d5..00000000 --- a/src/async_http/tests/id_rsa +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4ZtdaIrd1BPIJ -tfnF0TjIK5inQAXZ3XlCrUlJdP+XHwIRxdv1FsN12XyMYO/6ymLmo9ryoQeIrsXB -XYqlET3zfAY+diwCb0HEsVvhisthwMU4gZQu6TYW2s9LnXZB5rVtcBK69hcSlA2k -ZudMZWxZcj0L7KMfO2rIvaHw/qaVOE9j0T257Z8Kp2CLF9MUgX0ObhIsdumFRLaL -DvDUmBPr2zuh/34j2XmWwn1yjN/WvGtdfhXW79Ki1S40HcWnygHgLV8sESFKUxxQ -mKvPUTwDOIwLFL5WtE8Mz7N++kgmDcmWMCHc8kcOIu73Ta/3D4imW7VbKgHZo9+K -3ESFE3RjAgMBAAECggEBAJTEIyjMqUT24G2FKiS1TiHvShBkTlQdoR5xvpZMlYbN -tVWxUmrAGqCQ/TIjYnfpnzCDMLhdwT48Ab6mQJw69MfiXwc1PvwX1e9hRscGul36 -ryGPKIVQEBsQG/zc4/L2tZe8ut+qeaK7XuYrPp8bk/X1e9qK5m7j+JpKosNSLgJj -NIbYsBkG2Mlq671irKYj2hVZeaBQmWmZxK4fw0Istz2WfN5nUKUeJhTwpR+JLUg4 -ELYYoB7EO0Cej9UBG30hbgu4RyXA+VbptJ+H042K5QJROUbtnLWuuWosZ5ATldwO -u03dIXL0SH0ao5NcWBzxU4F2sBXZRGP2x/jiSLHcqoECgYEA4qD7mXQpu1b8XO8U -6abpKloJCatSAHzjgdR2eRDRx5PMvloipfwqA77pnbjTUFajqWQgOXsDTCjcdQui -wf5XAaWu+TeAVTytLQbSiTsBhrnoqVrr3RoyDQmdnwHT8aCMouOgcC5thP9vQ8Us -rVdjvRRbnJpg3BeSNimH+u9AHgsCgYEA0EzcbOltCWPHRAY7B3Ge/AKBjBQr86Kv -TdpTlxePBDVIlH+BM6oct2gaSZZoHbqPjbq5v7yf0fKVcXE4bSVgqfDJ/sZQu9Lp -PTeV7wkk0OsAMKk7QukEpPno5q6tOTNnFecpUhVLLlqbfqkB2baYYwLJR3IRzboJ -FQbLY93E8gkCgYB+zlC5VlQbbNqcLXJoImqItgQkkuW5PCgYdwcrSov2ve5r/Acz -FNt1aRdSlx4176R3nXyibQA1Vw+ztiUFowiP9WLoM3PtPZwwe4bGHmwGNHPIfwVG -m+exf9XgKKespYbLhc45tuC08DATnXoYK7O1EnUINSFJRS8cezSI5eHcbQKBgQDC -PgqHXZ2aVftqCc1eAaxaIRQhRmY+CgUjumaczRFGwVFveP9I6Gdi+Kca3DE3F9Pq -PKgejo0SwP5vDT+rOGHN14bmGJUMsX9i4MTmZUZ5s8s3lXh3ysfT+GAhTd6nKrIE -kM3Nh6HWFhROptfc6BNusRh1kX/cspDplK5x8EpJ0QKBgQDWFg6S2je0KtbV5PYe -RultUEe2C0jYMDQx+JYxbPmtcopvZQrFEur3WKVuLy5UAy7EBvwMnZwIG7OOohJb -vkSpADK6VPn9lbqq7O8cTedEHttm6otmLt8ZyEl3hZMaL3hbuRj6ysjmoFKx6CrX -rK0/Ikt5ybqUzKCMJZg2VKGTxg== ------END PRIVATE KEY----- diff --git a/src/async_http/tests/id_rsa.pub b/src/async_http/tests/id_rsa.pub deleted file mode 100644 index e8d62885..00000000 --- a/src/async_http/tests/id_rsa.pub +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4 -yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9 -83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs -WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT -69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8 -AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0 -YwIDAQAB ------END PUBLIC KEY----- diff --git a/src/async_http/tests/id_rsa1.pub b/src/async_http/tests/id_rsa1.pub deleted file mode 100644 index 5bc6dae3..00000000 --- a/src/async_http/tests/id_rsa1.pub +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4 -yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9 -83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs -WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT -69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8 -AziMCxS+VrRPDM+zfvpIJg3JlkAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0 -YwIDAQAB ------END PUBLIC KEY----- \ No newline at end of file diff --git a/src/async_http/tests/test_http_server.cpp b/src/async_http/tests/test_http_server.cpp deleted file mode 100644 index da83846b..00000000 --- a/src/async_http/tests/test_http_server.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// -// Created by Kadayam, Hari on 12/14/18. -// -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include "http_server.hpp" - -SISL_LOGGING_INIT(httpserver_lmod) -SISL_OPTIONS_ENABLE(logging) - -namespace { -sisl::HttpServerConfig s_cfg; -std::unique_ptr< sisl::HttpServer > s_server; -std::mutex s_m; -std::condition_variable s_cv; -bool s_is_shutdown{false}; -std::unique_ptr< std::thread > s_timer_thread; -} // namespace - -static void sleep_and_return(sisl::HttpCallData cd, int64_t secs) { - std::this_thread::sleep_for(std::chrono::seconds{secs}); - std::ostringstream ss{}; - ss << "Took a good nap for " << secs << " seconds. Thank you!\n"; - s_server->respond_OK(cd, EVHTP_RES_OK, ss.str()); -} - -static void delayed_return(sisl::HttpCallData cd) { - const auto req{cd->request()}; - const auto t{::evhtp_kvs_find_kv(req->uri->query, "seconds")}; - if (!t) { - s_server->respond_NOTOK(cd, EVHTP_RES_BADREQ, "Invalid seconds param!"); - return; - } - - std::string sstr{t->val}; - if (sstr.empty() || !std::all_of(sstr.begin(), sstr.end(), ::isdigit)) { - s_server->respond_NOTOK(cd, EVHTP_RES_BADREQ, - "Invalid seconds param! Either empty or contains non-digit characters\n"); - return; - } - - const int64_t secs{std::stoll(sstr, nullptr, 10)}; - s_timer_thread = std::make_unique< std::thread >(sleep_and_return, cd, secs); - return; -} - -static void say_hello(sisl::HttpCallData cd) { - std::cout << "Client is saying hello\n"; - s_server->respond_OK(cd, EVHTP_RES_OK, "Hello client from async_http server\n"); -} - -static void say_name(sisl::HttpCallData cd) { - s_server->respond_OK(cd, EVHTP_RES_OK, "I am the sisl (sizzling) http server \n"); -} - -static void shutdown(sisl::HttpCallData cd) { - std::cout << "Client is asking us to shutdown server\n"; - s_server->respond_OK(cd, EVHTP_RES_OK, "Ok will do\n"); - - { - std::lock_guard< std::mutex > lk{s_m}; - s_is_shutdown = true; - } - s_cv.notify_one(); -} - -class HTTPServerTest : public ::testing::Test { -public: - HTTPServerTest() = default; - HTTPServerTest(const HTTPServerTest&) = delete; - HTTPServerTest& operator=(const HTTPServerTest&) = delete; - HTTPServerTest(HTTPServerTest&&) noexcept = delete; - HTTPServerTest& operator=(HTTPServerTest&&) noexcept = delete; - virtual ~HTTPServerTest() override = default; - - virtual void SetUp() override { - s_server = std::make_unique< sisl::HttpServer >( - s_cfg, - std::vector< sisl::_handler_info >{handler_info("/api/v1/sayHello", say_hello, nullptr), - handler_info("/api/v1/shutdown", shutdown, nullptr), - handler_info("/api/v1/sleepFor", delayed_return, nullptr)}); - s_is_shutdown = false; - s_server->start(); - } - - virtual void TearDown() override { - s_server->stop(); - - if (s_timer_thread && s_timer_thread->joinable()) { s_timer_thread->join(); } - s_timer_thread.reset(); - s_server.reset(); - } - -protected: - void wait_for_shutdown() { - std::unique_lock< std::mutex > lk{s_m}; - s_cv.wait(lk, [] { return (s_is_shutdown); }); - } -}; - -TEST_F(HTTPServerTest, BasicTest) { - s_server->register_handler_info(handler_info("/api/v1/yourNamePlease", say_name, nullptr)); - - const cpr::Url url{"http://127.0.0.1:5051/api/v1/shutdown"}; - const auto resp{cpr::Post(url)}; - - ASSERT_EQ(resp.status_code, cpr::status::HTTP_OK); - - wait_for_shutdown(); - -#ifdef _PRERELEASE - std::cout << "ObjectLife Counter:\n"; - sisl::ObjCounterRegistry::foreach ([](const std::string& name, int64_t created, int64_t alive) { - std::cout << name << ": " << alive << "/" << created << "\n"; - }); -#endif -} - -TEST_F(HTTPServerTest, ParallelTestWithWait) { - s_server->register_handler_info(handler_info("/api/v1/yourNamePlease", say_name, nullptr)); - - std::atomic< bool > failed{false}; - const auto thread_func{[&failed](const size_t iterations) { - const cpr::Url url{"http://127.0.0.1:5051/api/v1/yourNamePlease"}; - for (size_t iteration{0}; (iteration < iterations) && !failed; ++iteration) { - const auto resp{cpr::Post(url)}; - if (resp.status_code != cpr::status::HTTP_OK) failed = true; - } - }}; - - constexpr size_t num_iterations{100}; - const size_t num_threads{std::max< size_t >(std::thread::hardware_concurrency(), 2)}; - std::vector< std::thread > workers; - for (size_t thread_num{0}; thread_num < num_threads; ++thread_num) { - workers.emplace_back(thread_func, num_iterations); - } - - for (auto& worker : workers) { - if (worker.joinable()) worker.join(); - } - - ASSERT_FALSE(failed); -} - -TEST_F(HTTPServerTest, ParallelTestWithoutWait) { - s_server->register_handler_info(handler_info("/api/v1/yourNamePlease", say_name, nullptr)); - - const auto thread_func{[](const size_t iterations) { - const cpr::Url url{"http://127.0.0.1:5051/api/v1/yourNamePlease"}; - for (size_t iteration{0}; iteration < iterations; ++iteration) { - [[maybe_unused]] auto response{cpr::PostAsync(url)}; - } - }}; - - constexpr size_t num_iterations{100}; - const size_t num_threads{std::max< size_t >(std::thread::hardware_concurrency(), 2)}; - std::vector< std::thread > workers; - for (size_t thread_num{0}; thread_num < num_threads; ++thread_num) { - workers.emplace_back(thread_func, num_iterations); - } - - for (auto& worker : workers) { - if (worker.joinable()) worker.join(); - } - - // exit while server processing -} - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - SISL_OPTIONS_LOAD(argc, argv, logging) - - s_cfg.is_tls_enabled = false; - s_cfg.bind_address = "127.0.0.1"; - s_cfg.server_port = 5051; - s_cfg.read_write_timeout_secs = 10; - - return RUN_ALL_TESTS(); -} diff --git a/src/auth_manager/CMakeLists.txt b/src/auth_manager/CMakeLists.txt deleted file mode 100644 index 41132d4a..00000000 --- a/src/auth_manager/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required (VERSION 3.10) - -add_flags("-Wno-unused-parameter") - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -find_package(FlatBuffers REQUIRED) - -set(AUTH_MGR_SOURCE_FILES - auth_manager.cpp - ) -add_library(sisl_auth_manager OBJECT ${AUTH_MGR_SOURCE_FILES}) -target_link_libraries(sisl_auth_manager - ${COMMON_DEPS} - cpr::cpr - flatbuffers::flatbuffers - jwt-cpp::jwt-cpp - ) -message("Flatbuffers parser: [${FLATBUFFERS_FLATC_EXECUTABLE}]") -settings_gen_cpp(${FLATBUFFERS_FLATC_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/generated/ sisl_auth_manager security_config.fbs) - -set(TRF_CLIENT_SOURCE_FILES - trf_client.cpp - ) -add_library(sisl_trf_client OBJECT ${TRF_CLIENT_SOURCE_FILES}) -target_link_libraries(sisl_trf_client - ${COMMON_DEPS} - cpr::cpr - flatbuffers::flatbuffers - ) -settings_gen_cpp(${FLATBUFFERS_FLATC_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/generated/ sisl_trf_client security_config.fbs) diff --git a/src/auth_manager/auth_manager.cpp b/src/auth_manager/auth_manager.cpp deleted file mode 100644 index 89663a54..00000000 --- a/src/auth_manager/auth_manager.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include -#include - -#include - -#include "auth_manager.hpp" - -namespace sisl { - -AuthVerifyStatus AuthManager::verify(const std::string& token, std::string& msg) const { - std::string app_name; - // TODO: cache tokens for better performance - try { - // this may throw if token is ill formed - const auto decoded{jwt::decode(token)}; - - // for any reason that causes the verification failure, an - // exception is thrown. - verify_decoded(decoded); - app_name = get_app(decoded); - } catch (const std::exception& e) { - msg = e.what(); - return AuthVerifyStatus::UNAUTH; - } - - // check client application - - if (SECURITY_DYNAMIC_CONFIG(auth_manager->auth_allowed_apps) != "all") { - if (SECURITY_DYNAMIC_CONFIG(auth_manager->auth_allowed_apps).find(app_name) == std::string::npos) { - msg = fmt::format("application '{}' is not allowed to perform the request", app_name); - return AuthVerifyStatus::FORBIDDEN; - } - } - - return AuthVerifyStatus::OK; -} -void AuthManager::verify_decoded(const jwt::decoded_jwt& decoded) const { - const auto alg{decoded.get_algorithm()}; - if (alg != "RS256") throw std::runtime_error(fmt::format("unsupported algorithm: {}", alg)); - - if (!decoded.has_header_claim("x5u")) throw std::runtime_error("no indication of verification key"); - - auto key_url = decoded.get_header_claim("x5u").as_string(); - - if (key_url.rfind(SECURITY_DYNAMIC_CONFIG(auth_manager->tf_token_url), 0) != 0) { - throw std::runtime_error(fmt::format("key url {} is not trusted", key_url)); - } - const std::string signing_key{download_key(key_url)}; - const auto verifier{jwt::verify() - .with_issuer(SECURITY_DYNAMIC_CONFIG(auth_manager->issuer)) - .allow_algorithm(jwt::algorithm::rs256(signing_key)) - .expires_at_leeway(SECURITY_DYNAMIC_CONFIG(auth_manager->leeway))}; - - // if verification fails, an instance of std::system_error subclass is thrown. - verifier.verify(decoded); -} - -std::string AuthManager::download_key(const std::string& key_url) const { - cpr::Session session; - session.SetUrl(cpr::Url{key_url}); - if (SECURITY_DYNAMIC_CONFIG(auth_manager->verify)) { - auto ca_file{SECURITY_DYNAMIC_CONFIG(ssl_ca_file)}; - auto cert_file{SECURITY_DYNAMIC_CONFIG(ssl_cert_file)}; - auto key_file{SECURITY_DYNAMIC_CONFIG(ssl_key_file)}; - - // constructor for CaInfo does std::move(filename) - auto sslOpts{cpr::Ssl(cpr::ssl::CaInfo{std::move(ca_file)})}; - sslOpts.SetOption(cpr::ssl::CertFile{std::move(cert_file)}); - sslOpts.SetOption(cpr::ssl::KeyFile{std::move(key_file)}); - session.SetOption(sslOpts); - } - - session.SetTimeout(std::chrono::milliseconds{5000}); - const auto resp{session.Get()}; - - if (resp.error) { throw std::runtime_error(fmt::format("download key failed: {}", resp.error.message)); } - if (resp.status_code != 200) { throw std::runtime_error(fmt::format("download key failed: {}", resp.text)); } - - return resp.text; -} - -std::string AuthManager::get_app(const jwt::decoded_jwt& decoded) const { - // get app name from client_id, which is the "sub" field in the decoded token - // body - // https://pages.github.corp.ebay.com/security-platform/documents/tf-documentation/tessintegration/#environment-variables - if (!decoded.has_payload_claim("sub")) return ""; - - const auto client_id{decoded.get_payload_claim("sub").as_string()}; - const auto start{client_id.find(",o=") + 3}; - const auto end{client_id.find_first_of(",", start)}; - return client_id.substr(start, end - start); -} -} // namespace sisl diff --git a/src/auth_manager/auth_manager.hpp b/src/auth_manager/auth_manager.hpp deleted file mode 100644 index 45ad6213..00000000 --- a/src/auth_manager/auth_manager.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include - -#undef HTTP_OK // nameclash with cpr/cpr.h header -#include - -// maybe-uninitialized variable in one of the included headers from jwt.h -#if defined __clang__ or defined __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" -#endif -#include -#if defined __clang__ or defined __GNUC__ -#pragma GCC diagnostic pop -#endif - -#include "utility/enum.hpp" -#include "security_config.hpp" - -namespace sisl { - -ENUM(AuthVerifyStatus, uint8_t, OK, UNAUTH, FORBIDDEN) - -class AuthManager { -public: - AuthManager() {} - virtual ~AuthManager() = default; - AuthVerifyStatus verify(const std::string& token, std::string& msg) const; - -private: - void verify_decoded(const jwt::decoded_jwt& decoded) const; - virtual std::string download_key(const std::string& key_url) const; - std::string get_app(const jwt::decoded_jwt& decoded) const; -}; -} // namespace sisl diff --git a/src/auth_manager/security_config.fbs b/src/auth_manager/security_config.fbs deleted file mode 100644 index def2fac2..00000000 --- a/src/auth_manager/security_config.fbs +++ /dev/null @@ -1,51 +0,0 @@ -native_include "utility/non_null_ptr.hpp"; -namespace securitycfg; - -attribute "hotswap"; -attribute "deprecated"; - -table TrfClient { - // grant token mount directory - grant_path: string; - - // Server addr to download the token (default: https://trustfabric.vip.ebay.com/v2/token) - server: string; - - // Pod env variables - app_name: string; - app_inst_name: string; - app_env: string; - pod_name: string; -} - -table AuthManager { - // , separated allowed applications (default all) - auth_allowed_apps: string; - - // Token issuer (default trustfabric) - issuer: string; - - // signing key domain - tf_token_url: string; - - // leeway to the token expiration - leeway: uint32 = 0; - - // ssl verification for the signing key download url - verify: bool = true; -} - -table SecuritySettings { - // ssl cert related files - ssl_cert_file: string; - ssl_key_file: string; - ssl_ca_file: string; - - // Auth Manager to decode and verify the token - auth_manager: AuthManager; - - // Trustfabric client settings - trf_client: TrfClient; -} - -root_type SecuritySettings; diff --git a/src/auth_manager/security_config.hpp b/src/auth_manager/security_config.hpp deleted file mode 100644 index cf33d7b1..00000000 --- a/src/auth_manager/security_config.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once -#include "settings/settings.hpp" -#include "options/options.h" -#include "generated/security_config_generated.h" - -SETTINGS_INIT(securitycfg::SecuritySettings, security_config) - -#define SECURITY_DYNAMIC_CONFIG_WITH(...) SETTINGS(security_config, __VA_ARGS__) -#define SECURITY_DYNAMIC_CONFIG_THIS(...) SETTINGS_THIS(security_config, __VA_ARGS__) -#define SECURITY_DYNAMIC_CONFIG(...) SETTINGS_VALUE(security_config, __VA_ARGS__) - -#define SECURITY_SETTINGS_FACTORY() SETTINGS_FACTORY(security_config) - -class SecurityDynamicConfig { -public: - static constexpr std::string_view default_auth_allowed_apps = "all"; - - static std::string get_env(const std::string& env_str) { - auto env_var = getenv(env_str.c_str()); - return (env_var != nullptr) ? std::string(env_var) : ""; - } - - inline static const std::string default_app_name{get_env("APP_NAME")}; - inline static const std::string default_app_inst_name{get_env("APP_INST_NAME")}; - inline static const std::string default_pod_name{get_env("POD_NAME")}; - inline static const std::string default_app_env{get_env("APP_ENV")}; - inline static const std::string default_ssl_cert_file{get_env("SSL_CERT")}; - inline static const std::string default_ssl_key_file{get_env("SSL_KEY")}; - inline static const std::string default_tf_token_url{get_env("TOKEN_URL")}; - inline static const std::string default_issuer{get_env("TOKEN_ISSUER")}; - inline static const std::string default_server{get_env("TOKEN_SERVER")}; - inline static const std::string default_grant_path{get_env("TOKEN_GRANT")}; - - // This method sets up the default for settings factory when there is no override specified in the json - // file and .fbs cannot specify default because they are not scalar. - static void init_settings_default() { - bool is_modified = false; - SECURITY_SETTINGS_FACTORY().modifiable_settings([&is_modified](auto& s) { - auto& ssl_cert_file = s.ssl_cert_file; - if (ssl_cert_file.empty()) { - ssl_cert_file = default_ssl_cert_file; - is_modified = true; - } - auto& ssl_key_file = s.ssl_key_file; - if (ssl_key_file.empty()) { - ssl_key_file = default_ssl_key_file; - is_modified = true; - } - auto& server = s.trf_client->server; - if (server.empty()) { - server = std::string_view(default_server); - is_modified = true; - } - auto& grant_path = s.trf_client->grant_path; - if (grant_path.empty()) { - grant_path = std::string_view(default_grant_path); - is_modified = true; - } - auto& auth_allowed_apps = s.auth_manager->auth_allowed_apps; - if (auth_allowed_apps.empty()) { - auth_allowed_apps = default_auth_allowed_apps; - is_modified = true; - } - auto& issuer = s.auth_manager->issuer; - if (issuer.empty()) { - issuer = default_issuer; - is_modified = true; - } - auto& tf_token_url = s.auth_manager->tf_token_url; - if (tf_token_url.empty()) { - tf_token_url = default_tf_token_url; - is_modified = true; - } - auto& app_name = s.trf_client->app_name; - if (app_name.empty()) { - app_name = std::string_view(default_app_name); - is_modified = true; - } - auto& app_inst_name = s.trf_client->app_inst_name; - if (app_inst_name.empty()) { - app_inst_name = std::string_view(default_app_inst_name); - is_modified = true; - } - auto& app_env = s.trf_client->app_env; - if (app_name.empty()) { - app_env = std::string_view(default_app_env); - is_modified = true; - } - auto& pod_name = s.trf_client->pod_name; - if (pod_name.empty()) { - pod_name = std::string_view(default_pod_name); - is_modified = true; - } - - // Any more default overrides or set non-scalar entries come here - }); - - if (is_modified) { SECURITY_SETTINGS_FACTORY().save(); } - } -}; diff --git a/src/auth_manager/trf_client.cpp b/src/auth_manager/trf_client.cpp deleted file mode 100644 index 6ef8fbbb..00000000 --- a/src/auth_manager/trf_client.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include - -#include "trf_client.hpp" - -namespace sisl { -TrfClient::TrfClient() { validate_grant_path(); } - -void TrfClient::validate_grant_path() const { - uint8_t retry_limit{10}; - // Retry until the grant path is up. Might take few seconds when deployed as tess secret - while (!grant_path_exists() && retry_limit-- > 0) { - std::this_thread::sleep_for(std::chrono::seconds{1}); - } - if (!grant_path_exists()) { - throw std::runtime_error{fmt::format("trustfabric client grant path {} does not exist", - SECURITY_DYNAMIC_CONFIG(trf_client->grant_path))}; - } -} - -bool TrfClient::get_file_contents(const std::string& file_path, std::string& contents) { - try { - std::ifstream f{file_path}; - const std::string buffer{std::istreambuf_iterator< char >{f}, std::istreambuf_iterator< char >{}}; - contents = buffer; - return !contents.empty(); - } catch (...) {} - return false; -} - -void TrfClient::request_with_grant_token() { - std::string grant_token; - if (!get_file_contents(SECURITY_DYNAMIC_CONFIG(trf_client->grant_path), grant_token)) { - throw std::runtime_error( - fmt::format("could not load grant from path {}", SECURITY_DYNAMIC_CONFIG(trf_client->grant_path))); - } - - const auto client_id{ - fmt::format("ou={}+l={},o={},dc=tess,dc=ebay,dc=com", SECURITY_DYNAMIC_CONFIG(trf_client->app_inst_name), - SECURITY_DYNAMIC_CONFIG(trf_client->app_env), SECURITY_DYNAMIC_CONFIG(trf_client->app_name))}; - - cpr::Session session; - if (SECURITY_DYNAMIC_CONFIG(auth_manager->verify)) { - auto ca_file{SECURITY_DYNAMIC_CONFIG(ssl_ca_file)}; - auto cert_file{SECURITY_DYNAMIC_CONFIG(ssl_cert_file)}; - auto key_file{SECURITY_DYNAMIC_CONFIG(ssl_key_file)}; - auto sslOpts{cpr::Ssl(cpr::ssl::CaInfo{std::move(ca_file)})}; - sslOpts.SetOption(cpr::ssl::CertFile{std::move(cert_file)}); - sslOpts.SetOption(cpr::ssl::KeyFile{std::move(key_file)}); - session.SetOption(sslOpts); - } - - session.SetUrl(cpr::Url{SECURITY_DYNAMIC_CONFIG(trf_client->server)}); - std::vector< cpr::Pair > payload_data; - payload_data.emplace_back("grant_type", "authorization_code"); - payload_data.emplace_back("code", grant_token); - payload_data.emplace_back("client_id", client_id); - session.SetPayload(cpr::Payload(payload_data.begin(), payload_data.end())); - session.SetTimeout(std::chrono::milliseconds{5000}); - const auto resp{session.Post()}; - if (resp.error || resp.status_code != 200) { - // TODO: log error, rest call failed - return; - } - - try { - const nlohmann::json resp_json = nlohmann::json::parse(resp.text); - const std::string expires_in{resp_json["expires_in"]}; - m_expiry = std::chrono::system_clock::now() + std::chrono::seconds(std::stoi(expires_in)); - m_access_token = resp_json["access_token"]; - m_token_type = resp_json["token_type"]; - } catch ([[maybe_unused]] const nlohmann::detail::exception& e) { - // TODO: log error, parsing failed - return; - } -} - -std::string TrfClient::get_token() { - if (m_access_token.empty() || access_token_expired()) { request_with_grant_token(); } - return m_access_token; -} -} // namespace sisl diff --git a/src/auth_manager/trf_client.hpp b/src/auth_manager/trf_client.hpp deleted file mode 100644 index e9eba20c..00000000 --- a/src/auth_manager/trf_client.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#undef HTTP_OK // nameclash with cpr/cpr.h header -#include -#include -#include "security_config.hpp" - -namespace sisl { - -class TrfClient { -public: - TrfClient(); - std::string get_token(); - std::string get_typed_token() { - const auto token_str{get_token()}; - return fmt::format("{} {}", m_token_type, token_str); - } - -private: - void validate_grant_path() const; - bool grant_path_exists() const { return std::filesystem::exists(SECURITY_DYNAMIC_CONFIG(trf_client->grant_path)); } - bool access_token_expired() const { - return (std::chrono::system_clock::now() > - m_expiry + std::chrono::seconds(SECURITY_DYNAMIC_CONFIG(auth_manager->leeway))); - } - static bool get_file_contents(const std::string& file_name, std::string& contents); - -protected: - virtual void request_with_grant_token(); - -protected: - std::string m_access_token; - std::string m_token_type; - std::chrono::system_clock::time_point m_expiry; -}; - -} // namespace sisl diff --git a/src/cache/CMakeLists.txt b/src/cache/CMakeLists.txt index d7961930..592ec8b4 100644 --- a/src/cache/CMakeLists.txt +++ b/src/cache/CMakeLists.txt @@ -1,26 +1,35 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-unused-parameter -Wno-cast-function-type") -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -set(CACHE_SOURCE_FILES +add_library(sisl_cache OBJECT) +target_sources(sisl_cache PRIVATE lru_evictor.cpp - ) -add_library(sisl_cache OBJECT ${CACHE_SOURCE_FILES}) + ) target_link_libraries(sisl_cache ${COMMON_DEPS}) -set(TEST_RANGEHASH_SOURCE_FILES - tests/test_range_hashmap.cpp - ) -add_executable(test_range_hashmap ${TEST_RANGEHASH_SOURCE_FILES}) -target_link_libraries(test_range_hashmap sisl ${COMMON_DEPS} Folly::Folly GTest::gtest) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_range_hashmap) + target_sources(test_range_hashmap PRIVATE + tests/test_range_hashmap.cpp + ) + target_include_directories(test_range_hashmap BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(test_range_hashmap sisl ${COMMON_DEPS} folly::folly GTest::gtest) + #add_test(NAME RangeHashMap COMMAND test_range_hashmap --num_iters 10000) -set(TEST_RANGECACHE_SOURCE_FILES - tests/test_range_cache.cpp - ) -add_executable(test_range_cache ${TEST_RANGECACHE_SOURCE_FILES}) -target_link_libraries(test_range_cache sisl ${COMMON_DEPS} Folly::Folly GTest::gtest) + add_executable(test_range_cache) + target_sources(test_range_cache PRIVATE + tests/test_range_cache.cpp + ) + target_include_directories(test_range_cache BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(test_range_cache sisl ${COMMON_DEPS} folly::folly GTest::gtest) + #add_test(NAME RangeCache COMMAND test_range_cache --num_iters 1000) + + add_executable(test_simple_cache) + target_sources(test_simple_cache PRIVATE + tests/test_simple_cache.cpp + ) + target_include_directories(test_simple_cache BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(test_simple_cache sisl ${COMMON_DEPS} folly::folly GTest::gtest) + add_test(NAME SimpleCache COMMAND test_simple_cache --num_iters 1000) + endif() +endif() diff --git a/src/cache/lru_evictor.cpp b/src/cache/lru_evictor.cpp index 7e6949f4..1ba796d5 100644 --- a/src/cache/lru_evictor.cpp +++ b/src/cache/lru_evictor.cpp @@ -8,93 +8,103 @@ * You may obtain a copy of the License at * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * Unless required by applicable law or agreed to in writing, software + *distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + *WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + *License for the specific language governing permissions and limitations under + *the License. * *********************************************************************************/ -#include "lru_evictor.hpp" +#include namespace sisl { -LRUEvictor::LRUEvictor(const int64_t max_size, const uint32_t num_partitions) : Evictor(max_size, num_partitions) { - m_partitions = std::make_unique< LRUPartition[] >(num_partitions); - for (uint32_t i{0}; i < num_partitions; ++i) { - m_partitions[i].init(this, i, uint64_cast(max_size / num_partitions)); - } +LRUEvictor::LRUEvictor(const int64_t max_size, const uint32_t num_partitions) + : Evictor(max_size, num_partitions) { + m_partitions = std::make_unique(num_partitions); + for (uint32_t i{0}; i < num_partitions; ++i) { + m_partitions[i].init(this, i, uint64_cast(max_size / num_partitions)); + } } -bool LRUEvictor::add_record(uint64_t hash_code, CacheRecord& record) { - return get_partition(hash_code).add_record(record); +bool LRUEvictor::add_record(uint64_t hash_code, CacheRecord &record) { + return get_partition(hash_code).add_record(record); } -void LRUEvictor::remove_record(uint64_t hash_code, CacheRecord& record) { - get_partition(hash_code).remove_record(record); +void LRUEvictor::remove_record(uint64_t hash_code, CacheRecord &record) { + get_partition(hash_code).remove_record(record); } -void LRUEvictor::record_accessed(uint64_t hash_code, CacheRecord& record) { - get_partition(hash_code).record_accessed(record); +void LRUEvictor::record_accessed(uint64_t hash_code, CacheRecord &record) { + get_partition(hash_code).record_accessed(record); } -void LRUEvictor::record_resized(uint64_t hash_code, const CacheRecord& record, uint32_t old_size) { - get_partition(hash_code).record_resized(record, old_size); +void LRUEvictor::record_resized(uint64_t hash_code, const CacheRecord &record, + uint32_t old_size) { + get_partition(hash_code).record_resized(record, old_size); } -bool LRUEvictor::LRUPartition::add_record(CacheRecord& record) { - std::unique_lock guard{m_list_guard}; - if (will_fill(record.size()) > m_max_size) { - if (!do_evict(record.record_family_id(), record.size())) { return false; } +bool LRUEvictor::LRUPartition::add_record(CacheRecord &record) { + std::unique_lock guard{m_list_guard}; + if (will_fill(record.size()) > m_max_size) { + if (!do_evict(record.record_family_id(), record.size())) { + return false; } - m_list.push_back(record); - m_filled_size += record.size(); - return true; + } + m_list.push_back(record); + m_filled_size += record.size(); + return true; } -void LRUEvictor::LRUPartition::remove_record(CacheRecord& record) { - std::unique_lock guard{m_list_guard}; - auto it = m_list.iterator_to(record); - m_filled_size -= record.size(); - m_list.erase(it); +void LRUEvictor::LRUPartition::remove_record(CacheRecord &record) { + std::unique_lock guard{m_list_guard}; + auto it = m_list.iterator_to(record); + m_filled_size -= record.size(); + m_list.erase(it); } -void LRUEvictor::LRUPartition::record_accessed(CacheRecord& record) { - std::unique_lock guard{m_list_guard}; - m_list.erase(m_list.iterator_to(record)); - m_list.push_back(record); +void LRUEvictor::LRUPartition::record_accessed(CacheRecord &record) { + std::unique_lock guard{m_list_guard}; + m_list.erase(m_list.iterator_to(record)); + m_list.push_back(record); } -void LRUEvictor::LRUPartition::record_resized(const CacheRecord& record, const uint32_t old_size) { - std::unique_lock guard{m_list_guard}; - m_filled_size -= (record.size() - old_size); +void LRUEvictor::LRUPartition::record_resized(const CacheRecord &record, + const uint32_t old_size) { + std::unique_lock guard{m_list_guard}; + m_filled_size -= (record.size() - old_size); } -bool LRUEvictor::LRUPartition::do_evict(const uint32_t record_fid, const uint32_t needed_size) { - size_t count{0}; +bool LRUEvictor::LRUPartition::do_evict(const uint32_t record_fid, + const uint32_t needed_size) { + size_t count{0}; - auto it = std::begin(m_list); - while (will_fill(needed_size) && (it != std::end(m_list))) { - CacheRecord& rec = *it; + auto it = std::begin(m_list); + while (will_fill(needed_size) && (it != std::end(m_list))) { + CacheRecord &rec = *it; - /* return the next element */ - if (!rec.is_pinned() && m_evictor->can_evict_cb(record_fid)(rec)) { - m_filled_size -= rec.size(); - it = m_list.erase(it); - } else { - ++count; - it = std::next(it); - } + /* return the next element */ + if (!rec.is_pinned() && m_evictor->can_evict_cb(record_fid)(rec)) { + m_filled_size -= rec.size(); + it = m_list.erase(it); + } else { + ++count; + it = std::next(it); } + } - if (count) { LOGDEBUG("LRU ejection had to skip {} entries", count); } - if (is_full()) { - // No available candidate to evict - LOGERROR("No cache space available: Eviction partition={} as total_entries={} rejected eviction request to add " - "size={}, already filled={}", - m_partition_num, m_list.size(), needed_size, m_filled_size); - return false; - } + if (count) { + LOGDEBUG("LRU ejection had to skip {} entries", count); + } + if (is_full()) { + // No available candidate to evict + LOGERROR("No cache space available: Eviction partition={} as " + "total_entries={} rejected eviction request to add " + "size={}, already filled={}", + m_partition_num, m_list.size(), needed_size, m_filled_size); + return false; + } - return true; + return true; } } // namespace sisl diff --git a/src/cache/tests/test_range_cache.cpp b/src/cache/tests/test_range_cache.cpp index 5f5b283b..843b321a 100644 --- a/src/cache/tests/test_range_cache.cpp +++ b/src/cache/tests/test_range_cache.cpp @@ -15,7 +15,6 @@ * *********************************************************************************/ #include -#include "options/options.h" #include #include #include @@ -27,10 +26,11 @@ #include #endif -#include "logging/logging.h" -#include "range_cache.hpp" -#include "lru_evictor.hpp" -#include "utility/enum.hpp" +#include +#include +#include +#include +#include using namespace sisl; SISL_LOGGING_INIT(test_rangecache) @@ -152,15 +152,15 @@ struct RangeCacheTest : public testing::Test { } } - void file_write(const uint32_t chunk_num, const uint32_t start_blk, const sisl::io_blob& b) { - const auto written = ::pwrite(m_fds[chunk_num], voidptr_cast(b.bytes), b.size, (start_blk * g_blk_size)); - RELEASE_ASSERT_EQ(written, b.size, "Not entire data is written to file"); + void file_write(const uint32_t chunk_num, const uint32_t start_blk, sisl::io_blob& b) { + const auto written = ::pwrite(m_fds[chunk_num], voidptr_cast(b.bytes()), b.size(), (start_blk * g_blk_size)); + RELEASE_ASSERT_EQ(written, b.size(), "Not entire data is written to file"); } sisl::io_blob file_read(const uint32_t chunk_num, const uint32_t blk, const uint32_t nblks) { sisl::io_blob b{nblks * g_blk_size, 0}; - const auto read_size = ::pread(m_fds[chunk_num], voidptr_cast(b.bytes), b.size, (blk * g_blk_size)); - RELEASE_ASSERT_EQ(uint32_cast(read_size), b.size, "Not entire data is read from file"); + const auto read_size = ::pread(m_fds[chunk_num], voidptr_cast(b.bytes()), b.size(), (blk * g_blk_size)); + RELEASE_ASSERT_EQ(uint32_cast(read_size), b.size(), "Not entire data is read from file"); return b; } @@ -168,7 +168,7 @@ struct RangeCacheTest : public testing::Test { auto b = file_read(chunk_num, data.first.m_nth, data.first.m_count); ASSERT_EQ(data.second.size(), data.first.m_count * g_blk_size) << "Mismatch of size between byte_view and RangeKey"; - auto ret = ::memcmp(data.second.bytes(), b.bytes, b.size); + auto ret = ::memcmp(data.second.bytes(), b.bytes(), b.size()); ASSERT_EQ(ret, 0) << "Data validation failed for Blk [" << data.first.m_nth << "-" << data.first.end_nth() << "]"; b.buf_free(); diff --git a/src/cache/tests/test_range_hashmap.cpp b/src/cache/tests/test_range_hashmap.cpp index f2910c77..d6de8557 100644 --- a/src/cache/tests/test_range_hashmap.cpp +++ b/src/cache/tests/test_range_hashmap.cpp @@ -15,105 +15,19 @@ * *********************************************************************************/ #include -#include "options/options.h" +#include #include #include #include -#include "logging/logging.h" -#include "fds/bitset.hpp" -#include "range_hashmap.hpp" -#include "utility/enum.hpp" +#include +#include +#include "sisl/fds/bitset.hpp" +#include using namespace sisl; SISL_LOGGING_INIT(test_hashmap) -#if 0 -struct TestRangeKey { - uint64_t m_num{0}; - uint32_t m_count{0}; - - TestRangeKey() = default; - TestRangeKey(const uint64_t n, const uint32_t c) : m_num{n}, m_count{c} {} - - std::pair< koffset_t, uint32_t > get_subset_from(const TestRangeKey& base) { - return std::make_pair<>(m_num - base.m_num, base.m_num + base.m_count - m_num); - } - - TestRangeKey get_base_key(const uint32_t split_boundary) { - return TestRangeKey{sisl::round_down(key.m_num, split_boundary), split_boundary}; - } - - // Take the range key and split them by modulo and then returning n different keyviews of it - static void split(const TestRangeKey& key, const uint32_t split_boundary, std::vector< KeyView >& out_views) { - auto base_num = sisl::round_down(key.m_num, split_boundary); - - auto base_key = key.get_base_key(split_boundary); - auto [offset, sub_size] = key.get_subset_from(base_key); - if (sub_size > key.m_count) { sub_size = key.m_count; } - out_views.emplace_back(base_key, offset, sub_size); - - auto remain_count = key.m_count - sub_size; - while (remain_count > 0) { - base_key.m_num += split_boundary; - size = std::min(split_boundary, remain_count); - out_views.emplace_back(base_key, 0, size); - remain_count -= size; - } - } - - static sisl::blob get_blob(const TestRangeKey& k) { - return sisl::blob{r_cast< uint8_t* >(&k), sizeof(TestRangeKey)}; - } - - static int compare(const TestRangeKey& k1, const TestRangeKey& k2) { - if (k2.m_num < k1.m_num) { - return -1; - } else if (k2.m_num > k1.m_num) { - return 1; - } else if (k2.m_count < k1.m_count) { - return -1; - } else if (k2.m_count > k1.m_count) { - return 1; - } else { - return 0; - } - } - - std::string to_string() const { return fmt::format("[{}-{}]", m_num, m_num + m_count - 1); } -}; - -struct TestRangeValue { -public: - TestRangeValue(const uint64_t& d1, const uint64_t& offset = 0) : m_base{d1}, m_offset{offset}, m_refcount{1} {} - TestRangeValue(const TestRangeValue& v) = default; - - bool operator==(const TestRangeValue& v) const { return (m_base == v.m_base); } - static std::string to_string(const TestRangeValue& v) { return fmt::format("{}", v.m_base + v.m_offset); } - - static void extract(TestRangeValue* v, const koffset_range_t& extract_range, uint8_t* new_buf) { - if (new_buf != nullptr) { - new (new_buf) TestRangeValue(v->m_base, v->m_offset + extract_range.second); - } else { - v->m_offset += extract_range.first; - } - } - - static bool can_erase(const TestRangeKey& k, const TestRangeValue& v) { return (--v.m_refcount == 0); } - - static void update(const TestRangeKey& base_key, const koffset_range_t range, TestRangeValue* value) { - ++value->m_refcount; - } - -private: - uint64_t m_base; - uint64_t m_offset; - int m_refcount; -}; - -DECLARE_RELOCATABLE(TestRangeValue) -#endif - static uint32_t g_max_offset; static constexpr uint32_t per_val_size = 128; @@ -146,11 +60,11 @@ struct RangeHashMapTest : public testing::Test { for (const auto& [key, val] : entries) { ASSERT_EQ(key.m_base_key, 1u) << "Expected base key is standard value 1"; - uint8_t* got_bytes = val.bytes(); + uint8_t const* got_bytes = val.bytes(); for (auto o{key.m_nth}; o < key.m_nth + key.m_count; ++o) { auto it = m_shadow_map.find(o); ASSERT_EQ(m_inserted_slots.is_bits_set(o, 1), true) << "Found a key " << o << " which was not inserted"; - compare_data(o, got_bytes, it->second.bytes); + compare_data(o, got_bytes, it->second.cbytes()); got_bytes += per_val_size; } } @@ -173,7 +87,7 @@ struct RangeHashMapTest : public testing::Test { sisl::io_blob create_data(const uint32_t start, const uint32_t end) { auto blob = sisl::io_blob{per_val_size * (end - start + 1), 0}; - uint8_t* bytes = blob.bytes; + uint8_t* bytes = blob.bytes(); for (auto i = start; i <= end; ++i) { auto arr = (std::array< uint32_t, per_val_size / sizeof(uint32_t) >*)bytes; @@ -271,228 +185,6 @@ TEST_F(RangeHashMapTest, RandomEverythingTest) { nblks_read, ninsert_ops, nblks_inserted, nerase_ops, nblks_erased); } -#if 0 -struct HashNodeTest : public testing::Test { -public: - virtual ~HashNodeTest() override = default; - -protected: - void SetUp() override { m_node = MultiEntryHashNode< TestRangeKey, TestRangeValue >::alloc_node(TestRangeKey{}); } - void TearDown() override { delete m_node; } - - void insert_range(const uint8_t start, const uint8_t end, const bool expected_success) { - bool is_resized; - uint64_t d = g_rand_generator(g_re); - auto val = TestRangeValue{d, 0}; - - std::tie(m_node, is_resized) = MultiEntryHashNode< TestRangeKey, TestRangeValue >::resize_if_needed(m_node, 1); - auto [v, success] = m_node->try_emplace(std::make_pair<>(start, end), val); - - ASSERT_EQ(expected_success, success) << "emplace returned unexpected status"; - if (success) { - auto i{start}; - while (true) { - m_shadow_map.insert({i, val}); - m_inserted_slots.set_bit(i); - if (i == end) break; - ++i; - } - } - } - - void validate_range(const uint8_t start, const uint8_t end) const { - std::vector< const MultiEntryHashNode< TestRangeKey, TestRangeValue >::val_entry_info* > entries; - int n = m_node->find(koffset_range_t{start, end}, entries); - ASSERT_EQ(entries.size(), s_cast< size_t >(n)) << "find return does not match vector entries"; - - for (const auto& e : entries) { - for (auto o{e->range.first}; o < e->range.second; ++o) { - auto it = m_shadow_map.find(o); - ASSERT_EQ(*(e->get_value_const()), it->second) << "Value mismatch for offset=" << (int)o; - } - } - } - - void erase_range(const uint8_t start, const uint8_t end, const uint8_t expected_count) { - bool is_resized; - std::tie(m_node, is_resized) = MultiEntryHashNode< TestRangeKey, TestRangeValue >::resize_if_needed(m_node, 1); - auto erased_count = m_node->erase(std::make_pair<>(start, end), TestRangeValue::extract); - ASSERT_EQ(erased_count, expected_count) - << "erase return of count does not match expected for range" << start << "-" << end; - if (erased_count == 0) { return; } - - auto i{start}; - while (true) { - m_shadow_map.erase(i); - m_inserted_slots.reset_bit(i); - if (i == end) break; - ++i; - } - } - - void validate_all(const uint8_t in_count_of = 8) { - LOGDEBUG("INFO: Read it back (and validate) in range of {}", in_count_of); - for (uint16_t k{0}; k <= 256 - in_count_of; k += in_count_of) { - validate_range(k, k + in_count_of); - } - m_node->validate_keys(); - } - - std::pair< koffset_t, koffset_t > pick_to_erase(koffset_t max_nblks) { - assert(m_shadow_map.size() != 0); - auto start{g_offset_generator(g_re)}; - uint64_t prev{start}; - koffset_t count{0}; - - max_nblks = std::min((g_max_offset - start) + 1, s_cast< uint32_t >(max_nblks)); - do { - auto b = m_inserted_slots.get_next_set_bit(prev); - if (b == prev) { - ++prev; - ++count; - } else if (count > 0) { - break; - } else if (b == sisl::Bitset::npos) { - start = 0; - prev = 0; - } else { - start = b; - prev = b + 1; - count = 1; - } - } while (count < max_nblks); - - assert(count > 0); - return std::make_pair(s_cast< koffset_t >(start), s_cast< koffset_t >(start + count - 1)); - } - - std::pair< koffset_t, koffset_t > pick_to_insert(const koffset_t max_nblks) { - assert(m_shadow_map.size() < g_max_offset + 1); - auto start_offset{g_offset_generator(g_re)}; - auto bb = m_inserted_slots.get_next_contiguous_n_reset_bits(start_offset, std::nullopt, 1, max_nblks); - if (bb.nbits == 0) { bb = m_inserted_slots.get_next_contiguous_n_reset_bits(0, std::nullopt, 1, max_nblks); } - assert(bb.nbits > 0); - return std::make_pair(s_cast< koffset_t >(bb.start_bit), s_cast< koffset_t >(bb.start_bit + bb.nbits - 1)); - } - -protected: - MultiEntryHashNode< TestRangeKey, TestRangeValue >* m_node; - std::unordered_map< uint8_t, TestRangeValue > m_shadow_map; - sisl::Bitset m_inserted_slots{g_max_offset + 1}; -}; - -TEST_F(HashNodeTest, SequentialTest) { - LOGINFO("INFO: Insert all items in the range of 4"); - for (uint16_t k{0}; k <= 252; k += 4) { - insert_range(k, k + 3, true); - insert_range(k, k + 1, false); - } - validate_all(); - - LOGINFO("INFO: Erase the middle of the range"); - for (uint16_t k{0}; k <= 252; k += 4) { - erase_range(k + 1, k + 2, 2); - } - validate_all(); - - LOGINFO("INFO: Erase the last in the range of 4"); - for (uint16_t k{0}; k <= 252; k += 4) { - erase_range(k + 3, k + 3, 1); - } - validate_all(); - - LOGINFO("INFO: ReInsert 2nd in the range"); - for (uint16_t k{0}; k <= 252; k += 4) { - insert_range(k + 1, k + 1, true); - } - validate_all(); - - LOGINFO("INFO: ReInsert 3rd in the range"); - for (uint16_t k{0}; k <= 252; k += 4) { - insert_range(k + 2, k + 2, true); - } - validate_all(); - - LOGINFO("Node details after test: {}", m_node->to_string()); -} - -TEST_F(HashNodeTest, RandomValidWriteTest) { - LOGINFO("INFO: Insert all items in the range of 4"); - uint32_t offset{0}; - while (offset < g_max_offset) { - const auto sz{g_size_generator(g_re)}; - const koffset_t s{s_cast< koffset_t >(offset)}; - LOGTRACE("Inserting range {} to {} cur_offset={}", s, - s + std::min(sz, s_cast< koffset_t >(g_max_offset - offset)), offset); - insert_range(s, s + std::min(sz, s_cast< koffset_t >(g_max_offset - offset)), true); - offset += sz + 1; - } - validate_all(); - LOGINFO("Node details after all insert: {}", m_node->to_string()); - - auto num_iters{SISL_OPTIONS["num_iters"].as< uint64_t >()}; - LOGINFO("INFO: Insert/Erase valid entries randomly for {} iterations", num_iters); - for (uint64_t i{0}; i < num_iters; ++i) { - if (m_shadow_map.size() < g_max_offset + 1) { - const auto [s, e] = pick_to_insert(g_size_generator(g_re)); - LOGTRACE("Inserting [{}-{}]:", s, e); - insert_range(s, e, true); - LOGTRACE("After insert node: {}", m_node->to_string()); - m_node->validate_keys(); - } - if (m_shadow_map.size() > 0) { - const auto [s, e] = pick_to_erase(g_size_generator(g_re)); - LOGTRACE("Erasing [{}-{}]:", s, e); - erase_range(s, e, e - s + 1); - LOGTRACE("After erase node: {}", m_node->to_string()); - m_node->validate_keys(); - } - } - LOGINFO("Node details after test: {}", m_node->to_string()); -} - -TEST_F(HashNodeTest, RandomEverythingTest) { - enum class op_t : uint8_t { read = 0, insert = 1, erase = 2 }; - uint32_t nread_ops{0}, ninsert_ops{0}, nerase_ops{0}; - uint32_t nblks_read{0}, nblks_inserted{0}, nblks_erased{0}; - - auto num_iters{SISL_OPTIONS["num_iters"].as< uint64_t >()}; - LOGINFO("INFO: Do completely random read/insert/erase operations with both valid and invalid entries for {} iters", - num_iters); - for (uint64_t i{0}; i < num_iters; ++i) { - const op_t op{s_cast< op_t >(g_op_generator(g_re))}; - const koffset_t offset{g_offset_generator(g_re)}; - koffset_t size{g_size_generator(g_re)}; - if (g_max_offset - offset + 1 < size) { size = g_max_offset - offset + 1; } - - switch (op) { - case op_t::read: - validate_range(offset, offset + size - 1); - nblks_read += m_inserted_slots.get_set_count(offset, offset + size - 1); - ++nread_ops; - break; - case op_t::insert: { - auto expected_inserts = size - m_inserted_slots.get_set_count(offset, offset + size - 1); - insert_range(offset, offset + size - 1, m_inserted_slots.is_bits_reset(offset, size)); - nblks_inserted += expected_inserts; - ++ninsert_ops; - break; - } - case op_t::erase: { - auto expected_erases = m_inserted_slots.get_set_count(offset, offset + size - 1); - erase_range(offset, offset + size - 1, expected_erases); - nblks_erased += expected_erases; - ++nerase_ops; - break; - } - } - } - LOGINFO("Node details after test: {}", m_node->to_string()); - LOGINFO("Executed read_ops={}, blks_read={} insert_ops={} blks_inserted={} erase_ops={} blks_erased={}", nread_ops, - nblks_read, ninsert_ops, nblks_inserted, nerase_ops, nblks_erased); -} -#endif - SISL_OPTIONS_ENABLE(logging, test_hashmap) SISL_OPTION_GROUP(test_hashmap, (max_offset, "", "max_offset", "max number of offset", diff --git a/src/cache/tests/test_simple_cache.cpp b/src/cache/tests/test_simple_cache.cpp new file mode 100644 index 00000000..6718a0cd --- /dev/null +++ b/src/cache/tests/test_simple_cache.cpp @@ -0,0 +1,200 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace sisl; +SISL_LOGGING_INIT(test_simplecache) + +static constexpr uint32_t g_val_size{512}; +static thread_local std::random_device g_rd{}; +static thread_local std::default_random_engine g_re{g_rd()}; + +struct Entry { + Entry(uint32_t id, const std::string& contents = "") : m_id{id}, m_contents{contents} {} + + uint32_t m_id; + std::string m_contents; +}; + +static constexpr std::array< const char, 62 > alphanum{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', + 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + +static std::string gen_random_string(size_t len) { + std::string str; + static thread_local std::random_device rd{}; + static thread_local std::default_random_engine re{rd()}; + std::uniform_int_distribution< size_t > rand_char{0, alphanum.size() - 1}; + for (size_t i{0}; i < len; ++i) { + str += alphanum[rand_char(re)]; + } + str += '\0'; + return str; +} + +struct SimpleCacheTest : public testing::Test { +protected: + std::shared_ptr< Evictor > m_evictor; + std::unique_ptr< SimpleCache< uint32_t, std::shared_ptr< Entry > > > m_cache; + std::unordered_map< uint32_t, std::string > m_shadow_map; + + uint64_t m_cache_misses{0}; + uint64_t m_cache_hits{0}; + uint32_t m_total_keys; + +protected: + void SetUp() override { + const auto cache_size = SISL_OPTIONS["cache_size_mb"].as< uint32_t >() * 1024 * 1024; + m_evictor = std::make_unique< LRUEvictor >(cache_size, 8); + m_cache = std::make_unique< SimpleCache< uint32_t, std::shared_ptr< Entry > > >( + m_evictor, // Evictor to evict used entries + cache_size / 4096, // Total number of buckets + g_val_size, // Value size + [](const std::shared_ptr< Entry >& e) -> uint32_t { return e->m_id; }, // Method to extract key + nullptr // Method to prevent eviction + ); + + const auto cache_pct = SISL_OPTIONS["cache_pct"].as< uint32_t >(); + const auto total_data_size = (100 * cache_size) / cache_pct; + m_total_keys = total_data_size / g_val_size; + LOGINFO("Initializing cache_size={} MB, cache_pct={}, total_data_size={}", + SISL_OPTIONS["cache_size_mb"].as< uint32_t >(), cache_pct, total_data_size); + } + + void TearDown() override { + m_evictor.reset(); + m_cache.reset(); + } + + void write(uint32_t id) { + const std::string data = gen_random_string(g_val_size); + const auto [it, expected_insert] = m_shadow_map.insert_or_assign(id, data); + + bool inserted = m_cache->upsert(std::make_shared< Entry >(id, data)); + ASSERT_EQ(inserted, expected_insert) + << "Mismatch about existence of key=" << id << " between shadow_map and cache"; + } + + void read(uint32_t id) { + const auto it = m_shadow_map.find(id); + bool expected_found = (it != m_shadow_map.end()); + + std::shared_ptr< Entry > e = std::make_shared< Entry >(0); + bool found = m_cache->get(id, e); + if (found) { + ASSERT_EQ(expected_found, true) << "Object key=" << id << " is deleted, but still found in cache"; + ASSERT_EQ(e->m_contents, it->second) << "Contents for key=" << id << " mismatch"; + ++m_cache_hits; + } else if (expected_found) { + bool inserted = m_cache->insert(std::make_shared< Entry >(id, it->second)); + ASSERT_EQ(inserted, true) << "Unable to insert to the cache for key=" << id; + ++m_cache_misses; + } + } + + void remove(uint32_t id) { + const auto it = m_shadow_map.find(id); + bool expected_found = (it != m_shadow_map.end()); + + std::shared_ptr< Entry > removed_e = std::make_shared< Entry >(0); + bool removed = m_cache->remove(id, removed_e); + if (removed) { + ASSERT_EQ(expected_found, true) + << "Object for key=" << id << " is deleted already, but still found in cache"; + ASSERT_EQ(removed_e->m_contents, it->second) << "Contents for key=" << id << " mismatch prior to removal"; + ++m_cache_hits; + } else { + ++m_cache_misses; + } + + m_shadow_map.erase(id); + } +}; + +VENUM(op_t, uint8_t, READ = 0, WRITE = 1, REMOVE = 2) + +TEST_F(SimpleCacheTest, RandomData) { + static std::uniform_int_distribution< uint8_t > op_generator{0, 2}; + static std::uniform_int_distribution< uint32_t > key_generator{0, this->m_total_keys}; + + uint32_t nread_ops{0}; + uint32_t nwrite_ops{0}; + uint32_t nremove_ops{0}; + + auto num_iters = SISL_OPTIONS["num_iters"].as< uint32_t >(); + LOGINFO("INFO: Do random read/write operations on all chunks for {} iters", num_iters); + for (uint32_t i{0}; i < num_iters; ++i) { + const op_t op = s_cast< op_t >(op_generator(g_re)); + const uint32_t id = key_generator(g_re); + + LOGDEBUG("INFO: Doing op={} for key=({})", enum_name(op), id); + switch (op) { + case op_t::READ: + read(id); + ++nread_ops; + break; + case op_t::WRITE: + write(id); + ++nwrite_ops; + break; + case op_t::REMOVE: + remove(id); + ++nremove_ops; + break; + } + } + const auto cache_ops = m_cache_hits + m_cache_misses; + LOGINFO("Executed read_ops={}, write_ops={} remove_ops={}", nread_ops, nwrite_ops, nremove_ops); + LOGINFO("Cache hits={} ({}%) Cache Misses={} ({}%)", m_cache_hits, (100 * (double)m_cache_hits) / cache_ops, + m_cache_misses, (100 * (double)m_cache_misses) / cache_ops); +} + +SISL_OPTIONS_ENABLE(logging, test_simplecache) +SISL_OPTION_GROUP(test_simplecache, + (cache_size_mb, "", "cache_size_mb", "cache size in mb", + ::cxxopts::value< uint32_t >()->default_value("100"), "number"), + (cache_pct, "", "cache_pct", "percentage of cache", + ::cxxopts::value< uint32_t >()->default_value("50"), "number"), + (num_iters, "", "num_iters", "number of iterations for rand ops", + ::cxxopts::value< uint32_t >()->default_value("65536"), "number")) + +int main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + SISL_OPTIONS_LOAD(argc, argv, logging, test_simplecache) + sisl::logging::SetLogger("test_simplecache"); + spdlog::set_pattern("[%D %T%z] [%^%L%$] [%t] %v"); + + auto ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/src/fds/CMakeLists.txt b/src/fds/CMakeLists.txt index 7455e22b..c00ee154 100644 --- a/src/fds/CMakeLists.txt +++ b/src/fds/CMakeLists.txt @@ -1,83 +1,109 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-unused-parameter -Wno-cast-function-type") -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) -set(BUFFER_SOURCE_FILES +add_library(sisl_buffer OBJECT) +target_sources(sisl_buffer PRIVATE buffer.cpp - ) -add_library(sisl_buffer OBJECT ${BUFFER_SOURCE_FILES}) -target_link_libraries(sisl_buffer ${COMMON_DEPS}) + ) +target_link_libraries(sisl_buffer folly::folly ${COMMON_DEPS}) -set(TEST_STREAM_TRACKER_SOURCES - tests/test_stream_tracker.cpp - ) -add_executable(test_stream_tracker ${TEST_STREAM_TRACKER_SOURCES}) -target_link_libraries(test_stream_tracker sisl ${COMMON_DEPS} GTest::gtest) -#add_test(NAME HttpServerTest COMMAND test_http_server) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_stream_tracker) + target_sources(test_stream_tracker PRIVATE + tests/test_stream_tracker.cpp + ) + target_link_libraries(test_stream_tracker sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME StreamTracker COMMAND test_stream_tracker) -set(GROUP_COMMIT_SOURCES - tests/group_commit.cpp - ) -add_executable(group_commit ${GROUP_COMMIT_SOURCES}) -target_link_libraries(group_commit sisl ${COMMON_DEPS}) + add_executable(test_atomic_status_counter) + target_sources(test_atomic_status_counter PRIVATE + tests/test_atomic_status_counter.cpp + ) + target_link_libraries(test_atomic_status_counter sisl ${COMMON_DEPS} GTest::gtest atomic) + add_test(NAME AtomicStatusCounter COMMAND test_atomic_status_counter) -set(TEST_ATOMIC_STATUS_COUNTER_SOURCES - tests/test_atomic_status_counter.cpp - ) -add_executable(test_atomic_status_counter ${TEST_ATOMIC_STATUS_COUNTER_SOURCES}) -target_link_libraries(test_atomic_status_counter sisl ${COMMON_DEPS} GTest::gtest atomic) -add_test(NAME atomic_status_counter COMMAND test_atomic_status_counter) + add_executable(test_bitset) + target_sources(test_bitset PRIVATE + tests/test_bitset.cpp + ) + target_link_libraries(test_bitset sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME Bitset COMMAND test_bitset) -set(TEST_BITSET_SOURCES - tests/test_bitset.cpp - ) -add_executable(test_bitset ${TEST_BITSET_SOURCES}) -target_link_libraries(test_bitset sisl ${COMMON_DEPS} GTest::gtest) -add_test(NAME bitset COMMAND test_bitset) + add_executable(test_bitword) + target_sources(test_bitword PRIVATE + tests/test_bitword.cpp + ) + target_link_libraries(test_bitword sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME Bitword COMMAND test_bitset) -set(TEST_BITWORD_SOURCES - tests/test_bitword.cpp - ) -add_executable(test_bitword ${TEST_BITWORD_SOURCES}) -target_link_libraries(test_bitword sisl ${COMMON_DEPS} GTest::gtest) + add_executable(test_compact_bitset) + target_sources(test_compact_bitset PRIVATE + tests/test_compact_bitset.cpp + ) + target_link_libraries(test_compact_bitset sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME CompactBitset COMMAND test_compact_bitset) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-attributes") # needed for C++ 20 folly compilation -endif() + add_executable(test_concurrent_insert_vector) + target_sources(test_concurrent_insert_vector PRIVATE + tests/test_concurrent_insert_vector.cpp + ) + target_link_libraries(test_concurrent_insert_vector sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME ConcurrentInsertVector COMMAND test_concurrent_insert_vector) + + add_executable(concurrent_insert_vector_bench) + target_sources(concurrent_insert_vector_bench PRIVATE + tests/concurrent_insert_vector_bench.cpp + ) + target_link_libraries(concurrent_insert_vector_bench sisl ${COMMON_DEPS} benchmark::benchmark) + add_test(NAME ConcurrentVectorBench COMMAND concurrent_insert_vector_bench) + + add_executable(obj_allocator_benchmark) + target_sources(obj_allocator_benchmark PRIVATE + tests/obj_allocator_benchmark.cpp + ) + target_link_libraries(obj_allocator_benchmark sisl ${COMMON_DEPS} benchmark::benchmark) + add_test(NAME ObjAllocatorBenchmark COMMAND obj_allocator_benchmark) + + add_executable(test_obj_allocator) + target_sources(test_obj_allocator PRIVATE + tests/test_obj_allocator.cpp + ) + target_link_libraries(test_obj_allocator sisl ${COMMON_DEPS}) + add_test(NAME ObjAlloc COMMAND test_obj_allocator) -set(OBJ_ALLOCATOR_BENCHMARK_FILES - tests/obj_allocator_benchmark.cpp - ) -add_executable(obj_allocator_benchmark ${OBJ_ALLOCATOR_BENCHMARK_FILES}) -target_link_libraries(obj_allocator_benchmark sisl ${COMMON_DEPS} benchmark::benchmark) -add_test(NAME ObjAllocatorBenchmark COMMAND obj_allocator_benchmark) + add_executable(test_cb_mutex) + target_sources(test_cb_mutex PRIVATE + tests/test_cb_mutex.cpp + ) + target_link_libraries(test_cb_mutex sisl ${COMMON_DEPS} GTest::gtest) + #add_test(NAME TestCBMutex COMMAND test_cb_mutex) -set(TEST_OBJALLOCATOR_SOURCE_FILES - tests/test_obj_allocator.cpp - ) -add_executable(test_obj_allocator ${TEST_OBJALLOCATOR_SOURCE_FILES}) -target_link_libraries(test_obj_allocator sisl ${COMMON_DEPS}) -add_test(NAME ObjAlloc COMMAND test_obj_allocator) + add_executable(test_sg_list) + target_sources(test_sg_list PRIVATE + tests/test_sg_list.cpp + ) + target_link_libraries(test_sg_list sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME SgList COMMAND test_sg_list) -set(TEST_CBMUTEX_SOURCE_FILES - tests/test_cb_mutex.cpp - ) -add_executable(test_cb_mutex ${TEST_CBMUTEX_SOURCE_FILES}) -target_link_libraries(test_cb_mutex sisl ${COMMON_DEPS} GTest::gtest) -add_test(NAME TestCBMutex COMMAND test_cb_mutex) -if (DEFINED MALLOC_IMPL) - if (${MALLOC_IMPL} STREQUAL "jemalloc") - set(TEST_JEMALLOC_SOURCE_FILES - tests/test_jemalloc_helper.cpp - ) - add_executable(test_jemalloc ${TEST_JEMALLOC_SOURCE_FILES}) - target_link_libraries(test_jemalloc sisl jemalloc) - add_test(NAME TestJemalloc COMMAND test_jemalloc) + if (DEFINED MALLOC_IMPL) + if (${MALLOC_IMPL} STREQUAL "jemalloc") + add_executable(test_jemalloc) + target_sources(test_jemalloc PRIVATE + tests/test_jemalloc_helper.cpp + ) + target_link_libraries(test_jemalloc sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME TestJemalloc COMMAND test_jemalloc) + elseif (${MALLOC_IMPL} STREQUAL "tcmalloc") + add_executable(test_tcmalloc) + target_sources(test_tcmalloc PRIVATE + tests/test_tcmalloc_helper.cpp + ) + target_link_libraries(test_tcmalloc sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME TestTcmalloc COMMAND test_tcmalloc) + endif() endif() + endif() endif() diff --git a/src/fds/buffer.cpp b/src/fds/buffer.cpp index d694755d..ff46f5fa 100644 --- a/src/fds/buffer.cpp +++ b/src/fds/buffer.cpp @@ -15,7 +15,7 @@ * *********************************************************************************/ #include -#include "buffer.hpp" +#include "sisl/fds/buffer.hpp" namespace sisl { uint8_t* AlignedAllocatorImpl::aligned_alloc(const size_t align, const size_t sz, const sisl::buftag tag) { diff --git a/src/fds/callback_mutex.hpp b/src/fds/callback_mutex.hpp index 60a7f30d..64c847b7 100644 --- a/src/fds/callback_mutex.hpp +++ b/src/fds/callback_mutex.hpp @@ -17,7 +17,8 @@ #include #include #include -#include "vector_pool.hpp" +#include "sisl/fds/vector_pool.hpp" +#include #include // Generate the metafunction @@ -38,8 +39,8 @@ class _cb_wait_q { _cb_wait_q() = default; ~_cb_wait_q() = default; - void add_cb(const post_lock_cb_t& cb) { - std::unique_lock< std::mutex > l(m_waitq_mutex); + void add_cb(post_lock_cb_t&& cb) { + folly::SharedMutexWritePriority::WriteHolder holder{m_waitq_lock}; if (m_wait_q == nullptr) { m_wait_q = sisl::VectorPool< post_lock_cb_t >::alloc(); } m_wait_q->emplace_back(std::move(cb)); } @@ -47,12 +48,12 @@ class _cb_wait_q { bool drain_cb() { std::vector< post_lock_cb_t >* wait_q{nullptr}; { - std::unique_lock< std::mutex > l(m_waitq_mutex); + folly::SharedMutexWritePriority::WriteHolder holder{m_waitq_lock}; std::swap(wait_q, m_wait_q); } if (wait_q) { - for (auto& cb : *wait_q) { + for (const auto& cb : *wait_q) { cb(); } sisl::VectorPool< post_lock_cb_t >::free(wait_q); @@ -60,8 +61,13 @@ class _cb_wait_q { return (wait_q != nullptr); } + bool empty() const { + folly::SharedMutexWritePriority::ReadHolder holder{m_waitq_lock}; + return ((m_wait_q == nullptr) || (m_wait_q->empty())); + } + private: - std::mutex m_waitq_mutex; + mutable folly::SharedMutexWritePriority m_waitq_lock; std::vector< post_lock_cb_t >* m_wait_q{nullptr}; }; @@ -71,7 +77,7 @@ class CallbackMutex { explicit CallbackMutex() = default; ~CallbackMutex() = default; - bool try_lock(const post_lock_cb_t& cb) { + bool try_lock(post_lock_cb_t&& cb) { if (m_base_mutex.try_lock()) { cb(); return true; @@ -81,7 +87,7 @@ class CallbackMutex { } template < class I = MutexImpl > - typename std::enable_if< try_lock_shared_check< I >, bool >::type try_lock_shared(const post_lock_cb_t& cb) { + typename std::enable_if< try_lock_shared_check< I >, bool >::type try_lock_shared(post_lock_cb_t&& cb) { if (m_base_mutex.try_lock_shared()) { cb(); return true; @@ -99,6 +105,15 @@ class CallbackMutex { template < class I = MutexImpl > typename std::enable_if< unlock_shared_check< I >, void >::type unlock_shared() { m_base_mutex.unlock_shared(); + if (!m_q.empty()) { + // If Q is not empty, try to lock the base mutex (which callers wait on) and if successful, + // we can drain the q. If unsuccessful, ignore it, because next unlock on elements in the q + // will do the same + if (m_base_mutex.try_lock()) { + m_q.drain_cb(); + m_base_mutex.unlock(); + } + } } template < class I = MutexImpl > @@ -127,8 +142,8 @@ class CallbackMutex { template < typename MutexImpl > class CBUniqueLock { public: - CBUniqueLock(CallbackMutex< MutexImpl >& cb_mtx, const post_lock_cb_t& cb) : m_cb_mtx{cb_mtx} { - m_locked = m_cb_mtx.try_lock(cb); + CBUniqueLock(CallbackMutex< MutexImpl >& cb_mtx, post_lock_cb_t&& cb) : m_cb_mtx{cb_mtx} { + m_locked = m_cb_mtx.try_lock(std::move(cb)); } ~CBUniqueLock() { @@ -143,8 +158,8 @@ class CBUniqueLock { template < typename MutexImpl > class CBSharedLock { public: - CBSharedLock(CallbackMutex< MutexImpl >& cb_mtx, const post_lock_cb_t& cb) : m_cb_mtx{cb_mtx} { - m_locked = m_cb_mtx.try_lock_shared(cb); + CBSharedLock(CallbackMutex< MutexImpl >& cb_mtx, post_lock_cb_t&& cb) : m_cb_mtx{cb_mtx} { + m_locked = m_cb_mtx.try_lock_shared(std::move(cb)); } ~CBSharedLock() { diff --git a/src/fds/memvector.hpp b/src/fds/memvector.hpp index 92557557..f4834bb3 100644 --- a/src/fds/memvector.hpp +++ b/src/fds/memvector.hpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include #include "buffer.hpp" diff --git a/src/fds/tests/concurrent_insert_vector_bench.cpp b/src/fds/tests/concurrent_insert_vector_bench.cpp new file mode 100644 index 00000000..7edc1e67 --- /dev/null +++ b/src/fds/tests/concurrent_insert_vector_bench.cpp @@ -0,0 +1,67 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include + +#include +#include +#include + +using namespace sisl; + +static constexpr uint32_t NUM_THREADS = 1; +std::unique_ptr< std::vector< uint64_t > > glob_lock_vector; +std::mutex glob_vector_mutex; + +std::unique_ptr< sisl::ConcurrentInsertVector< uint64_t > > glob_cvec; + +void test_locked_vector_insert(benchmark::State& state) { + // auto const per_thread_count = nentries / state.threads(); + + LOGINFO("Running on {} iterations in {} threads", state.iterations(), state.threads()); + std::cout << "Running on iterations=" << state.iterations() << " in threads=" << state.threads() << "\n"; + glob_lock_vector = std::make_unique< std::vector< uint64_t > >(); + + uint64_t i{0}; + for (auto _ : state) { // Loops upto iteration count + std::lock_guard< std::mutex > lg(glob_vector_mutex); + glob_lock_vector->emplace_back(++i); + } +} + +void test_concurrent_vector_insert(benchmark::State& state) { + std::cout << "Running on iterations=" << state.iterations() << " in threads=" << state.threads() << "\n"; + glob_cvec = std::make_unique< sisl::ConcurrentInsertVector< uint64_t > >(); + + uint64_t i{0}; + for (auto _ : state) { // Loops upto iteration count + glob_cvec->emplace_back(++i); + } +} + +BENCHMARK(test_locked_vector_insert)->Threads(NUM_THREADS); +BENCHMARK(test_concurrent_vector_insert)->Threads(NUM_THREADS); + +int main(int argc, char** argv) { + int parsed_argc{argc}; + ::benchmark::Initialize(&parsed_argc, argv); + + // setup(); + ::benchmark::RunSpecifiedBenchmarks(); +} diff --git a/src/fds/tests/group_commit.cpp b/src/fds/tests/group_commit.cpp deleted file mode 100644 index 06047c2f..00000000 --- a/src/fds/tests/group_commit.cpp +++ /dev/null @@ -1,379 +0,0 @@ -/********************************************************************************* - * Modifications Copyright 2017-2019 eBay Inc. - * - * Author/Developer(s): Harihara Kadayam - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - *********************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __linux__ -#include -#endif - -#include -#include "logging/logging.h" -#include - -#include "memvector.hpp" -#include "obj_allocator.hpp" -#include "stream_tracker.hpp" - -using namespace sisl; - -SISL_LOGGING_INIT(group_commit) - -#pragma pack(1) -struct log_group_header { - uint32_t magic; - uint32_t n_log_records; // Total number of log records - uint32_t group_size; // Total size of this group including this header - uint32_t inline_data_size; // Out of group size how much data is inlined along with log_record_header - uint32_t prev_grp_checksum; - uint32_t cur_grp_checksum; -}; -#pragma pack() - -template < typename charT, typename traits > -std::basic_ostream< charT, traits >& operator<<(std::basic_ostream< charT, traits >& outStream, - const log_group_header& h) { - // copy the stream formatting - std::basic_ostringstream< charT, traits > outStringStream; - outStringStream.copyfmt(outStream); - - // output the date time - const auto s{fmt::format("magic = {} n_log_records = {} group_size = {} inline_data_size = {} " - "prev_grp_checksum = {} cur_grp_checksum = {}", - h.magic, h.n_log_records, h.group_size, h.inline_data_size, h.prev_grp_checksum, - h.cur_grp_checksum)}; - outStringStream << s; - - // print the stream - outStream << outStringStream.str(); - - return outStream; -} - -#pragma pack(1) -struct serialized_log_record { - int64_t log_idx; - uint32_t size : 31; - uint32_t is_inlined : 1; - uint8_t data[1]; -}; -#pragma pack() - -static constexpr uint32_t dma_boundary{512}; - -class LogGroup; -struct log_record { - static constexpr uint32_t inline_size{dma_boundary}; - - serialized_log_record* pers_record{nullptr}; - uint8_t* data_ptr; - uint32_t size; - void* context; - LogGroup* log_group; // Group this record is part of - - log_record(uint8_t* const d, const uint32_t sz, void* const ctx) { - data_ptr = d; - size = sz; - context = ctx; - } - - serialized_log_record* create_serialized(uint8_t* const buf_ptr, const int64_t log_idx) const { - serialized_log_record* const sr{reinterpret_cast< serialized_log_record* >(buf_ptr)}; - sr->log_idx = log_idx; - sr->size = size; - sr->is_inlined = is_inlinebale(); - if (is_inlinebale()) { - std::memcpy(static_cast< void* >(sr->data), static_cast< const void* >(data_ptr), size); - } - return sr; - } - - // NOTE: serialized_log_record contains 1 byte of data already which is why - 1 - size_t inlined_size() const { - return sizeof(serialized_log_record) + (((size > 0) && is_inlinebale()) ? size - 1 : 0); - } - size_t serialized_size() const { return sizeof(serialized_log_record) + ((size > 0) ? (size - 1) : 0); } - bool is_inlinebale() const { return (size <= inline_size); } - static size_t serialized_size(const uint32_t sz) { - return sizeof(serialized_log_record) + ((sz > 0 ? (sz - 1) : 0)); - } -}; - -static constexpr uint32_t flush_idx_frequency{64}; - -struct iovec_wrapper : public iovec { - iovec_wrapper(void* const base, const size_t len) { - iov_base = base; - iov_len = len; - } -}; - -class LogGroup { - template < typename charT, typename traits > - friend std::basic_ostream< charT, traits >& operator<<(std::basic_ostream< charT, traits >& outStream, - const LogGroup& lg); - -public: - static constexpr uint32_t estimated_iovs{10}; - static constexpr size_t inline_log_buf_size{log_record::inline_size * flush_idx_frequency}; - static constexpr size_t max_log_group_size{8192}; - - typedef sisl::FlexArray< iovec_wrapper, estimated_iovs > iovec_array; - friend class LogDev; - - LogGroup() { - m_cur_log_buf = &m_log_buf[0]; - m_cur_buf_len = inline_log_buf_size; - m_cur_buf_pos = sizeof(log_group_header); - m_overflow_log_buf = nullptr; - m_nrecords = 0; - m_total_non_inlined_size = 0; - - m_iovecs.emplace_back(static_cast< void* >(m_cur_log_buf), 0); - } - - void create_overflow_buf(const uint32_t min_needed) { - const uint32_t new_len{std::max(min_needed, m_cur_buf_len * 2)}; - auto new_buf{std::unique_ptr< uint8_t[] >(new uint8_t[new_len])}; - std::memcpy(static_cast< void* >(new_buf.get()), static_cast< const void* >(m_cur_log_buf), m_cur_buf_len); - m_overflow_log_buf = std::move(new_buf); - m_cur_log_buf = m_overflow_log_buf.get(); - m_cur_buf_len = new_len; - m_iovecs[0].iov_base = static_cast< void* >(m_cur_log_buf); - } - - bool can_accomodate(const log_record& record) const { - return ((record.serialized_size() + m_cur_buf_pos + m_total_non_inlined_size) <= max_log_group_size); - } - - bool add_record(const log_record& record, const int64_t log_idx) { - if (!can_accomodate(record)) { - std::cout << "Will exceed max_log_group_size=" << max_log_group_size - << " if we add this record for idx=" << log_idx << " Hence stopping adding in this batch"; - return false; - } - - const auto size{record.inlined_size()}; - if ((m_cur_buf_pos + size) >= m_cur_buf_len) { create_overflow_buf(m_cur_buf_pos + size); } - - // If serialized size is within inline budget and also we have enough room to hold this data, we can copy - // them, instead of having a iovec element. - // std::cout << "size to insert=" << size << " inline_size=" << log_record::inline_size - // << " cur_buf_pos=" << m_cur_buf_pos << " inline_log_buf_size=" << inline_log_buf_size << "\n"; - record.create_serialized(&m_cur_log_buf[m_cur_buf_pos], log_idx); - m_cur_buf_pos += size; - m_iovecs[0].iov_len += size; - if (!record.is_inlinebale()) { - // TODO: Round this up to 512 byte boundary - m_iovecs.emplace_back(static_cast< void* >(record.data_ptr), record.size); - m_total_non_inlined_size += record.size; - } - m_nrecords++; - - return true; - } - - const iovec_array* finish() { - log_group_header* const hdr{header()}; - hdr->magic = 0xDABAF00D; - hdr->n_log_records = m_nrecords; - hdr->inline_data_size = m_cur_buf_pos; - hdr->group_size = hdr->inline_data_size + m_total_non_inlined_size; - hdr->prev_grp_checksum = 0; - hdr->cur_grp_checksum = 0; - - return &m_iovecs; - } - - log_group_header* header() const { return reinterpret_cast< log_group_header* >(m_cur_log_buf); } - iovec_array& iovecs() { return m_iovecs; } - - uint32_t data_size() const { return header()->group_size - sizeof(log_group_header); } - -private: - std::array< uint8_t, inline_log_buf_size > m_log_buf; - uint8_t* m_cur_log_buf{m_log_buf.data()}; - uint32_t m_cur_buf_len{inline_log_buf_size}; - uint32_t m_cur_buf_pos{sizeof(log_group_header)}; - - std::unique_ptr< uint8_t[] > m_overflow_log_buf; - - uint32_t m_nrecords{0}; - uint32_t m_total_non_inlined_size{0}; - - // Info about the final data - iovec_array m_iovecs; - int64_t m_flush_log_idx_from; - int64_t m_flush_log_idx_upto; - uint64_t m_log_dev_offset; -}; - -template < typename charT, typename traits > -std::basic_ostream< charT, traits >& operator<<(std::basic_ostream< charT, traits >& outStream, const LogGroup& lg) { - // copy the stream formatting - std::basic_ostringstream< charT, traits > outStringStream; - outStringStream.copyfmt(outStream); - - // output the date time - const auto s{fmt::format("-----------------------------------------------------------------\n" - "Header: [{}]\nLog_idx_range:[{} - {}] Offset={} non_inlined_size={}\n" - "-----------------------------------------------------------------\n", - *(reinterpret_cast< log_group_header* >(lg.m_cur_log_buf)), lg.m_flush_log_idx_from, - lg.m_flush_log_idx_upto, lg.m_log_dev_offset, lg.m_total_non_inlined_size)}; - outStringStream << s; - - // print the stream - outStream << outStringStream.str(); - - return outStream; -} - -typedef sisl::ObjectAllocator< LogGroup, 100 > LogGroupAllocator; - -class LogDev { -public: - typedef std::function< void(int64_t, uint64_t, void*) > log_append_cb_t; - void register_cb(const log_append_cb_t& cb) { m_append_cb = cb; } - - // static constexpr int64_t flush_threshold_size{ 4096 }; - static constexpr int64_t flush_threshold_size{100}; - static constexpr int64_t flush_data_threshold_size{flush_threshold_size - sizeof(log_group_header)}; - - int64_t append(uint8_t* const data, const uint32_t size, void* const cb_context) { - flush_if_needed(size); - const auto idx{m_log_idx.fetch_add(1, std::memory_order_acq_rel)}; - m_log_records.create(idx, data, size, cb_context); - return idx; - } - - void flush_if_needed(const uint32_t record_size) { - // If after adding the record size, if we have enough to flush, attempt to flush by setting the atomic bool - // variable. - const auto actual_size{record_size ? log_record::serialized_size(record_size) : 0}; - const auto pending_sz{m_pending_flush_size.fetch_add(actual_size, std::memory_order_relaxed) + actual_size}; - if (pending_sz >= flush_data_threshold_size) { - std::cout << "Pending size to flush is " << pending_sz << " greater than flush data threshold " - << flush_data_threshold_size << " Flushing now\n"; - bool expected_flushing = false; - if (m_is_flushing.compare_exchange_strong(expected_flushing, true, std::memory_order_acq_rel)) { - // We were able to win the flushing competition and now we gather all the flush data and reserve a slot. - auto lg = prepare_flush(); - m_pending_flush_size.fetch_sub(lg->data_size(), std::memory_order_relaxed); - std::cout << "After flushing prepared pending size is " << m_pending_flush_size.load() << "\n"; - dummy_do_io(lg->iovecs(), [lg, this](const bool success) { on_flush_completion(lg, success); }); - } - } - } - - LogGroup* prepare_flush() { - int64_t flushing_upto_idx{0}; - - auto* lg{LogGroupAllocator::make_object()}; - m_log_records.foreach_active(m_last_flush_idx + 1, - [&](const int64_t idx, const int64_t upto_idx, const log_record& record) -> bool { - if (lg->add_record(record, idx)) { - flushing_upto_idx = upto_idx; - return true; - } else { - return false; - } - }); - lg->finish(); - lg->m_flush_log_idx_from = m_last_flush_idx + 1; - lg->m_flush_log_idx_upto = flushing_upto_idx; - lg->m_log_dev_offset = reserve(lg->data_size() + sizeof(log_group_header)); - - std::cout << "Flushing upto log_idx = " << flushing_upto_idx << "\n"; - std::cout << "Log Group:\n" << *lg; - return lg; - } - - void on_flush_completion(LogGroup* const lg, const bool is_success) { - assert(is_success); - m_log_records.complete(lg->m_flush_log_idx_from, lg->m_flush_log_idx_upto); - m_last_flush_idx = lg->m_flush_log_idx_upto; - - for (auto idx = lg->m_flush_log_idx_from; idx <= lg->m_flush_log_idx_upto; ++idx) { - auto& record{m_log_records.at(idx)}; - m_append_cb(idx, lg->m_log_dev_offset, record.context); - } -#if 0 - if (upto_idx > (m_last_truncate_idx + LogDev::truncate_idx_frequency)) { - std::cout << "Truncating upto log_idx = " << upto_idx << "\n"; - m_log_records.truncate(); - } -#endif - m_is_flushing.store(false, std::memory_order_release); - LogGroupAllocator::deallocate(lg); - - // Try to do chain flush if its really needed. - flush_if_needed(0); - } - - void dummy_do_io(const LogGroup::iovec_array& iovecs, const std::function< void(bool) >& cb) { - // LOG INFO("iovecs with {} pieces", iovecs.size()); - for (size_t i{0}; i < iovecs.size(); ++i) { - std::cout << "Base = " << iovecs[i].iov_base << " Length = " << iovecs[i].iov_len << "\n"; - // LOGINFO("Base = {} Length = {}", iovec.iov_base, iovec.iov_len); - } - cb(true); - } - - uint64_t reserve(const uint32_t size) { - static uint64_t offset{0}; - auto cur_offset{offset}; - offset += size; - return cur_offset; - } - -public: - static constexpr uint32_t truncate_idx_frequency{flush_idx_frequency * 10}; - -private: - sisl::StreamTracker< log_record > m_log_records; - std::atomic< int64_t > m_log_idx{0}; - std::atomic< int64_t > m_pending_flush_size{0}; - std::atomic< bool > m_is_flushing{false}; - - // sisl::atomic_status_counter< flush_status_t, flush_status_t::Normal > m_flush_status; - int64_t m_last_flush_idx{-1}; - int64_t m_last_truncate_idx{-1}; - uint64_t m_offset{0}; - - log_append_cb_t m_append_cb; -}; - -static void on_append_completion(const int64_t idx, const uint64_t offset, void* const ctx) { - std::cout << "Append completed with log_idx = " << idx << " offset = " << offset << "\n"; -} - -int main(int argc, char* argv[]) { - std::array< std::string, 1024 > s; - LogDev ld; - ld.register_cb(on_append_completion); - - for (size_t i{0}; i < 200; ++i) { - s[i] = std::to_string(i); - ld.append(reinterpret_cast< uint8_t* >(const_cast< char* >(s[i].c_str())), s[i].size() + 1, nullptr); - } -} diff --git a/src/fds/tests/obj_allocator_benchmark.cpp b/src/fds/tests/obj_allocator_benchmark.cpp index 37605ea9..570f6d30 100644 --- a/src/fds/tests/obj_allocator_benchmark.cpp +++ b/src/fds/tests/obj_allocator_benchmark.cpp @@ -22,18 +22,18 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include "sisl/logging/logging.h" +#include "sisl/options/options.h" -#include "metrics/metrics.hpp" -#include "obj_allocator.hpp" +#include "sisl/metrics/metrics.hpp" +#include "sisl/fds/obj_allocator.hpp" SISL_LOGGING_INIT(HOMESTORE_LOG_MODS) RCU_REGISTER_INIT namespace { std::mutex s_print_mutex; -constexpr size_t ITERATIONS{1000000}; +constexpr size_t ITERATIONS{10000000}; constexpr size_t THREADS{8}; struct my_request { @@ -49,7 +49,7 @@ void test_malloc(benchmark::State& state) { static thread_local std::random_device rd{}; static thread_local std::default_random_engine engine{rd()}; - for (auto [[maybe_unused]] si : state) { // Loops up to iteration count + for ([[maybe_unused]] auto si : state) { // Loops up to iteration count my_request* req; benchmark::DoNotOptimize(req = new my_request()); req->m_a = 10; @@ -69,7 +69,7 @@ void test_obj_alloc(benchmark::State& state) { uint64_t counter{0}; static thread_local std::random_device rd{}; static thread_local std::default_random_engine engine{rd()}; - for (auto [[maybe_unused]] si : state) { // Loops up to iteration count + for ([[maybe_unused]] auto si : state) { // Loops up to iteration count my_request* req; benchmark::DoNotOptimize(req = sisl::ObjectAllocator< my_request >::make_object()); req->m_a = 10; @@ -79,6 +79,7 @@ void test_obj_alloc(benchmark::State& state) { counter += req->m_d; sisl::ObjectAllocator< my_request >::deallocate(req); } + { std::scoped_lock< std::mutex > lock{s_print_mutex}; std::cout << "Counter = " << counter << std::endl; diff --git a/src/fds/tests/test_atomic_status_counter.cpp b/src/fds/tests/test_atomic_status_counter.cpp index 6f80b08f..b960c75d 100644 --- a/src/fds/tests/test_atomic_status_counter.cpp +++ b/src/fds/tests/test_atomic_status_counter.cpp @@ -14,12 +14,12 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include "logging/logging.h" -#include "options/options.h" +#include +#include #include -#include "atomic_status_counter.hpp" +#include "sisl/fds/atomic_status_counter.hpp" using namespace sisl; diff --git a/src/fds/tests/test_bitset.cpp b/src/fds/tests/test_bitset.cpp index 2f4515c7..25fa3cbd 100644 --- a/src/fds/tests/test_bitset.cpp +++ b/src/fds/tests/test_bitset.cpp @@ -21,12 +21,12 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include #include -#include "bitset.hpp" +#include "sisl/fds/bitset.hpp" using namespace sisl; diff --git a/src/fds/tests/test_bitword.cpp b/src/fds/tests/test_bitword.cpp index 53a48054..7ac61772 100644 --- a/src/fds/tests/test_bitword.cpp +++ b/src/fds/tests/test_bitword.cpp @@ -20,12 +20,12 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include #include -#include "bitword.hpp" +#include "sisl/fds/bitword.hpp" using namespace sisl; diff --git a/src/fds/tests/test_cb_mutex.cpp b/src/fds/tests/test_cb_mutex.cpp index f07d594f..b1e10b35 100644 --- a/src/fds/tests/test_cb_mutex.cpp +++ b/src/fds/tests/test_cb_mutex.cpp @@ -17,8 +17,9 @@ #include #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include +#include #include #pragma GCC diagnostic push @@ -27,7 +28,7 @@ #pragma GCC diagnostic pop #include "callback_mutex.hpp" -#include "utils.hpp" +#include "sisl/fds/utils.hpp" SISL_LOGGING_INIT(test_cb_mutex) @@ -68,8 +69,7 @@ class CBMutexTest : public testing::Test { } template < typename I = MutexImpl > - typename std::enable_if< !sisl::CallbackMutex< I >::shared_mode_supported, void >::type - thread_shared_fn(uint64_t count_per_thread) { + typename std::enable_if< !sisl::CallbackMutex< I >::shared_mode_supported, void >::type thread_shared_fn(uint64_t) { assert(0); } @@ -84,24 +84,27 @@ class CBMutexTest : public testing::Test { shared_threads = std::max(1u, num_threads - unique_threads); } - std::vector< std::thread* > threads; + std::vector< std::thread > threads; for (uint32_t i{0}; i < unique_threads; ++i) { - threads.push_back(new std::thread(bind_this(CBMutexTest::thread_unique_fn, 1), num_iters / num_threads)); + threads.emplace_back( + std::move(std::thread(bind_this(CBMutexTest::thread_unique_fn, 1), num_iters / num_threads))); } for (uint32_t i{0}; i < shared_threads; ++i) { - threads.push_back(new std::thread(bind_this(CBMutexTest::thread_shared_fn<>, 1), num_iters / num_threads)); + threads.emplace_back( + std::move(std::thread(bind_this(CBMutexTest::thread_shared_fn<>, 1), num_iters / num_threads))); } - for (auto t : threads) { - t->join(); - delete (t); + for (auto& t : threads) { + t.join(); + // delete (t); } } }; using testing::Types; -// typedef Types< std::mutex, std::shared_mutex, folly::SharedMutex > Implementations; -typedef Types< std::mutex > Implementations; +typedef Types< std::mutex, std::shared_mutex, folly::SharedMutex > Implementations; +// typedef Types< std::mutex > Implementations; +// typedef Types< std::mutex, folly::SharedMutex > Implementations; TYPED_TEST_SUITE(CBMutexTest, Implementations); TYPED_TEST(CBMutexTest, LockUnlockTest) { this->run_lock_unlock(); } diff --git a/src/fds/tests/test_compact_bitset.cpp b/src/fds/tests/test_compact_bitset.cpp new file mode 100644 index 00000000..adc0ff64 --- /dev/null +++ b/src/fds/tests/test_compact_bitset.cpp @@ -0,0 +1,158 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include + +#include +#include + +#include + +#include + +using namespace sisl; + +SISL_OPTIONS_ENABLE(logging, test_compact_bitset) + +class CompactBitsetTest : public testing::Test { +protected: + sisl::io_blob_safe m_buf; + std::unique_ptr< CompactBitSet > m_bset; + +public: + CompactBitsetTest() : + testing::Test(), + m_buf{uint32_cast( + sisl::round_up(SISL_OPTIONS["buf_size"].as< uint32_t >(), CompactBitSet::size_multiples()))} {} + CompactBitsetTest(const CompactBitsetTest&) = delete; + CompactBitsetTest(CompactBitsetTest&&) noexcept = delete; + CompactBitsetTest& operator=(const CompactBitsetTest&) = delete; + CompactBitsetTest& operator=(CompactBitsetTest&&) noexcept = delete; + virtual ~CompactBitsetTest() override = default; + +protected: + void SetUp() override { m_bset = std::make_unique< CompactBitSet >(m_buf, true); } + void TearDown() override {} +}; + +TEST_F(CompactBitsetTest, AlternateBits) { + ASSERT_EQ(m_bset->size(), m_buf.size() * 8); + + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->is_bit_set(i), false); + } + + // Set alternate bits + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); i += 2) { + m_bset->set_bit(i); + } + + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->is_bit_set(i), (i % 2 == 0)); + } + + // Validate if next set or reset bit starting from itself returns itself back + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->get_next_set_or_reset_bit(i, ((i % 2) == 0)), i); + } + + // Validate if next set or reset bit starting from previous returns next bit + for (CompactBitSet::bit_count_t i{1}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->get_next_set_or_reset_bit(i - 1, ((i % 2) == 0)), i); + } +} + +TEST_F(CompactBitsetTest, AllBits) { + // Set all bits + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + m_bset->set_bit(i); + } + + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->is_bit_set(i), true); + } + + for (CompactBitSet::bit_count_t i{0}; i < m_bset->size(); ++i) { + ASSERT_EQ(m_bset->get_next_set_bit(i), i); + ASSERT_EQ(m_bset->get_next_reset_bit(i), CompactBitSet::inval_bit); + } +} + +TEST_F(CompactBitsetTest, RandomBitsWithReload) { + auto const num_bits = m_bset->size(); + boost::dynamic_bitset<> shadow_bset{num_bits}; + + std::random_device rd; + std::mt19937 re(rd()); + std::uniform_int_distribution< CompactBitSet::bit_count_t > bit_gen(0, num_bits - 1); + for (uint64_t i{0}; i < num_bits / 2; ++i) { + auto bit = bit_gen(re); + shadow_bset.set(bit); + m_bset->set_bit(s_cast< CompactBitSet::bit_count_t >(bit)); + } + + auto validate = [this, &shadow_bset]() { + CompactBitSet::bit_count_t prev_set_bit{CompactBitSet::inval_bit}; + for (uint64_t i{0}; i < m_bset->size(); ++i) { + auto next_shadow_set_bit = (i == 0) ? shadow_bset.find_first() : shadow_bset.find_next(i - 1); + CompactBitSet::bit_count_t next_set_bit = m_bset->get_next_set_bit(i); + if (next_shadow_set_bit == boost::dynamic_bitset<>::npos) { + ASSERT_EQ(next_set_bit, CompactBitSet::inval_bit); + } else { + ASSERT_EQ(next_set_bit, next_shadow_set_bit); + if (next_set_bit == i) { prev_set_bit = i; } + ASSERT_EQ(m_bset->get_prev_set_bit(i), prev_set_bit); + } + } + + // Flip it back so we can look for reset bits + shadow_bset = shadow_bset.flip(); + for (uint64_t i{0}; i < m_bset->size(); ++i) { + auto next_shadow_reset_bit = (i == 0) ? shadow_bset.find_first() : shadow_bset.find_next(i - 1); + CompactBitSet::bit_count_t next_reset_bit = m_bset->get_next_reset_bit(i); + if (next_shadow_reset_bit == boost::dynamic_bitset<>::npos) { + ASSERT_EQ(next_reset_bit, CompactBitSet::inval_bit); + } else { + ASSERT_EQ(next_reset_bit, next_shadow_reset_bit); + } + } + + // Flip it back to original + shadow_bset = shadow_bset.flip(); + }; + + validate(); + m_bset = std::make_unique< CompactBitSet >(m_buf, false); // Reload + validate(); +} + +SISL_OPTION_GROUP(test_compact_bitset, + (buf_size, "", "buf_size", "buf_size that contains the bits", + ::cxxopts::value< uint32_t >()->default_value("1024"), "number")) + +int main(int argc, char* argv[]) { + int parsed_argc{argc}; + ::testing::InitGoogleTest(&parsed_argc, argv); + SISL_OPTIONS_LOAD(parsed_argc, argv, logging, test_compact_bitset); + + sisl::logging::SetLogger("test_compact_bitset"); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + return RUN_ALL_TESTS(); +} diff --git a/src/fds/tests/test_concurrent_insert_vector.cpp b/src/fds/tests/test_concurrent_insert_vector.cpp new file mode 100644 index 00000000..38c4488e --- /dev/null +++ b/src/fds/tests/test_concurrent_insert_vector.cpp @@ -0,0 +1,116 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +using namespace sisl; + +SISL_OPTIONS_ENABLE(logging, test_concurrent_insert_vector) + +class ConcurrentInsertVectorTest : public testing::Test { +protected: + ConcurrentInsertVector< uint32_t > m_cvec; + std::vector< std::thread > m_threads; + +public: + ConcurrentInsertVectorTest() : testing::Test() {} + ConcurrentInsertVectorTest(const ConcurrentInsertVectorTest&) = delete; + ConcurrentInsertVectorTest(ConcurrentInsertVectorTest&&) noexcept = delete; + ConcurrentInsertVectorTest& operator=(const ConcurrentInsertVectorTest&) = delete; + ConcurrentInsertVectorTest& operator=(ConcurrentInsertVectorTest&&) noexcept = delete; + virtual ~ConcurrentInsertVectorTest() override = default; + +protected: + void insert_and_wait() { + auto const nthreads = SISL_OPTIONS["num_threads"].as< uint32_t >(); + auto const per_thread_count = SISL_OPTIONS["num_entries"].as< uint32_t >() / nthreads; + for (size_t i{0}; i < nthreads; ++i) { + m_threads.emplace_back( + [this](uint32_t start, uint32_t count) { + for (uint32_t i{0}; i < count; ++i) { + m_cvec.push_back(start + i); + } + }, + i * per_thread_count, per_thread_count); + } + + for (auto& thr : m_threads) { + thr.join(); + } + } + + void validate_all() { + sisl::Bitset bset{SISL_OPTIONS["num_entries"].as< uint32_t >()}; + m_cvec.foreach_entry([&bset](uint32_t const& e) { bset.set_bit(e); }); + ASSERT_EQ(bset.get_next_reset_bit(0), sisl::Bitset::npos) << "Access didn't receive all entries"; + ASSERT_EQ(m_cvec.size(), bset.get_set_count(0)) << "Size doesn't match with number of entries"; + } + + void validate_all_by_iteration() { + sisl::Bitset bset{SISL_OPTIONS["num_entries"].as< uint32_t >()}; + for (const auto& e : m_cvec) { + bset.set_bit(e); + } + ASSERT_EQ(bset.get_next_reset_bit(0), sisl::Bitset::npos) << "Access didn't receive all entries"; + ASSERT_EQ(m_cvec.size(), bset.get_set_count(0)) << "Size doesn't match with number of entries"; + } +}; + +TEST_F(ConcurrentInsertVectorTest, concurrent_insertion) { + LOGINFO("Step1: Inserting {} entries in parallel in {} threads and wait", + SISL_OPTIONS["num_entries"].as< uint32_t >(), SISL_OPTIONS["num_threads"].as< uint32_t >()); + insert_and_wait(); + + LOGINFO("Step2: Validating all entries are inserted"); + validate_all(); + + LOGINFO("Step3: Validating all entries again to ensure it is readable multipled times"); + validate_all(); + + LOGINFO("Step4: Validating all entries by iterator"); + validate_all_by_iteration(); + + LOGINFO("Step5: Validating all entries again by iterator to ensure it is readable multipled times"); + validate_all_by_iteration(); +} + +SISL_OPTION_GROUP(test_concurrent_insert_vector, + (num_entries, "", "num_entries", "num_entries", + ::cxxopts::value< uint32_t >()->default_value("10000"), "number"), + (num_threads, "", "num_threads", "num_threads", ::cxxopts::value< uint32_t >()->default_value("8"), + "number")) + +int main(int argc, char* argv[]) { + int parsed_argc{argc}; + ::testing::InitGoogleTest(&parsed_argc, argv); + SISL_OPTIONS_LOAD(parsed_argc, argv, logging, test_concurrent_insert_vector); + + sisl::logging::SetLogger("test_concurrent_insert_vector"); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + return RUN_ALL_TESTS(); +} diff --git a/src/fds/tests/test_idreserver.cpp b/src/fds/tests/test_idreserver.cpp index 251e9dc1..a519b32d 100644 --- a/src/fds/tests/test_idreserver.cpp +++ b/src/fds/tests/test_idreserver.cpp @@ -18,10 +18,10 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include -#include "id_reserver.hpp" +#include "sisl/fds/id_reserver.hpp" #include diff --git a/src/fds/tests/test_jemalloc_helper.cpp b/src/fds/tests/test_jemalloc_helper.cpp index 18d1e2ca..88ca01c2 100644 --- a/src/fds/tests/test_jemalloc_helper.cpp +++ b/src/fds/tests/test_jemalloc_helper.cpp @@ -23,14 +23,13 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include "sisl/logging/logging.h" +#include "sisl/options/options.h" +#include "sisl/utility/thread_buffer.hpp" #include -#include "utility/thread_buffer.hpp" - -#include "malloc_helper.hpp" +#include "sisl/fds/malloc_helper.hpp" using namespace sisl; diff --git a/src/fds/tests/test_obj_allocator.cpp b/src/fds/tests/test_obj_allocator.cpp index 06849f2b..54086aff 100644 --- a/src/fds/tests/test_obj_allocator.cpp +++ b/src/fds/tests/test_obj_allocator.cpp @@ -17,10 +17,10 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include "sisl/logging/logging.h" +#include "sisl/options/options.h" -#include "obj_allocator.hpp" +#include "sisl/fds/obj_allocator.hpp" SISL_LOGGING_INIT(HOMESTORE_LOG_MODS) @@ -42,7 +42,7 @@ class Node { }; } // namespace -int main(int argc, char** argv) { +int main() { Node< uint64_t >* const ptr1{sisl::ObjectAllocator< Node< uint64_t > >::make_object(~static_cast< uint64_t >(0))}; std::cout << "ptr1 = " << static_cast< const void* >(ptr1) << " Id = " << ptr1->get_id() << std::endl; sisl::ObjectAllocator< Node< uint64_t > >::deallocate(ptr1); diff --git a/src/fds/tests/test_sg_list.cpp b/src/fds/tests/test_sg_list.cpp new file mode 100644 index 00000000..c62f6789 --- /dev/null +++ b/src/fds/tests/test_sg_list.cpp @@ -0,0 +1,201 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ + +#include +#include +#include "sisl/fds/buffer.hpp" + +SISL_LOGGING_INIT(test_sg_list) +SISL_OPTIONS_ENABLE(logging, test_sg_list) +SISL_OPTION_GROUP(test_sg_list, + (num_threads, "", "num_threads", "number of threads", + ::cxxopts::value< uint32_t >()->default_value("8"), "number")) + +static constexpr uint32_t SZ{sizeof(uint32_t)}; + +// the iterator request size is same as iov size for each iov; +TEST(SgListTestBasic, TestIteratorAlignedSize) { + sisl::sg_iovs_t iovs; + iovs.push_back(iovec{nullptr, 1024}); + iovs.push_back(iovec{nullptr, 512}); + iovs.push_back(iovec{nullptr, 2048}); + iovs.push_back(iovec{nullptr, 512}); + uint32_t iov_size_total = 0; + for (const auto& v : iovs) { + iov_size_total += v.iov_len; + } + + sisl::sg_list sg; + sg.size = iov_size_total; + sg.iovs = iovs; + + sisl::sg_iterator sg_it{sg.iovs}; + std::vector< uint32_t > bids_size_vec{1024, 512, 2048, 512}; + uint32_t bids_size_total = 0; + for (const auto s : bids_size_vec) { + bids_size_total += s; + } + + ASSERT_EQ(iov_size_total, bids_size_total); + + uint32_t itr_size_total = 0; + for (const auto& size : bids_size_vec) { + const auto iovs = sg_it.next_iovs(size); + for (const auto& iov : iovs) { + itr_size_total += iov.iov_len; + } + } + + ASSERT_EQ(itr_size_total, bids_size_total); +} + +// +// the iterator request size is unaligned with iov len, but total size is same; +// +TEST(SgListTestBasic, TestIteratorUnalignedSize) { + sisl::sg_iovs_t iovs; + iovs.push_back(iovec{nullptr, 1024}); + iovs.push_back(iovec{nullptr, 512}); + iovs.push_back(iovec{nullptr, 2048}); + iovs.push_back(iovec{nullptr, 512}); + uint32_t iov_size_total = 0; + for (const auto& v : iovs) { + iov_size_total += v.iov_len; + } + + sisl::sg_list sg; + sg.size = iov_size_total; + sg.iovs = iovs; + + sisl::sg_iterator sg_it{sg.iovs}; + std::vector< uint32_t > bids_size_vec{512, 1024, 1024, 512, 512, 512}; + uint32_t bids_size_total = 0; + for (const auto s : bids_size_vec) { + bids_size_total += s; + } + + ASSERT_EQ(iov_size_total, bids_size_total); + + uint32_t itr_size_total = 0; + for (const auto& size : bids_size_vec) { + const auto iovs = sg_it.next_iovs(size); + for (const auto& iov : iovs) { + itr_size_total += iov.iov_len; + } + } + + ASSERT_EQ(itr_size_total, bids_size_total); +} + +class SgListTestOffset : public testing::Test { +public: + void SetUp() override { + + for (uint16_t i = 0; i < 8; ++i) { + data_vec.emplace_back(get_random_num()); + sgl.iovs.emplace_back(iovec{new uint32_t(data_vec[i]), SZ}); + } + sgl.size = SZ * 8; + } + + void TearDown() override { + for (auto& iov : sgl.iovs) { + auto data_ptr = r_cast< uint32_t* >(iov.iov_base); + delete data_ptr; + } + } + + static uint32_t get_random_num() { + static std::random_device dev; + static std::mt19937 rng(dev()); + std::uniform_int_distribution< std::mt19937::result_type > dist(1001u, 99999u); + return dist(rng); + } + + std::vector< uint32_t > data_vec; + sisl::sg_list sgl{0, {}}; +}; + +TEST_F(SgListTestOffset, TestMoveOffsetAligned) { + // test next_iovs and sg_list_to_ioblob_list + sisl::sg_iterator sgitr{sgl.iovs}; + auto ioblob_list = sisl::io_blob::sg_list_to_ioblob_list(sgl); + ASSERT_EQ(sgl.iovs.size(), ioblob_list.size()); + ASSERT_EQ(sgl.iovs.size(), data_vec.size()); + for (uint16_t i = 0; i < data_vec.size(); ++i) { + auto const iovs = sgitr.next_iovs(SZ); + ASSERT_EQ(iovs.size(), 1); + auto rand_num = r_cast< uint32_t* >(iovs[0].iov_base); + EXPECT_EQ(*rand_num, data_vec[i]); + + rand_num = r_cast< uint32_t* >(ioblob_list[i].bytes()); + EXPECT_EQ(*rand_num, data_vec[i]); + EXPECT_EQ(ioblob_list[i].size(), SZ); + } + + sisl::sg_iterator sgitr1{sgl.iovs}; + // test move_offset + for (uint16_t i = 0; i < data_vec.size(); ++i) { + if (i % 2 == 0) { + sgitr1.move_offset(SZ); + continue; + } + auto const iovs = sgitr1.next_iovs(SZ); + ASSERT_EQ(iovs.size(), 1); + auto rand_num = r_cast< uint32_t* >(iovs[0].iov_base); + EXPECT_EQ(*rand_num, data_vec[i]); + } +} + +TEST_F(SgListTestOffset, TestMoveOffsetUnaligned) { + // total size should be SZ * 8 + std::vector< uint32_t > size_vec{SZ, 3 * SZ, SZ / 2, SZ / 4, 2 * SZ, SZ / 4 + SZ}; + uint32_t itr_size_total{0}; + sisl::sg_iterator sgitr{sgl.iovs}; + for (auto const& s : size_vec) { + auto const iovs = sgitr.next_iovs(s); + for (const auto& iov : iovs) { + itr_size_total += iov.iov_len; + } + } + EXPECT_EQ(itr_size_total, sgl.size); + + sisl::sg_iterator sgitr1{sgl.iovs}; + uint32_t itr_size_offset{0}; + uint32_t itr_size_offset_target{0}; + for (uint16_t i = 0; i < size_vec.size(); ++i) { + if (i % 2 == 0) { + sgitr1.move_offset(size_vec[i]); + continue; + } + auto const iovs = sgitr1.next_iovs(size_vec[i]); + for (const auto& iov : iovs) { + itr_size_offset += iov.iov_len; + } + itr_size_offset_target += size_vec[i]; + } + EXPECT_EQ(itr_size_offset_target, itr_size_offset); +} + +int main(int argc, char* argv[]) { + int parsed_argc{argc}; + ::testing::InitGoogleTest(&parsed_argc, argv); + SISL_OPTIONS_LOAD(parsed_argc, argv, logging, test_sg_list); + sisl::logging::SetLogger("test_sg_list"); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + const auto ret{RUN_ALL_TESTS()}; + return ret; +} diff --git a/src/fds/tests/test_stream_tracker.cpp b/src/fds/tests/test_stream_tracker.cpp index 092165a8..bfe832e8 100644 --- a/src/fds/tests/test_stream_tracker.cpp +++ b/src/fds/tests/test_stream_tracker.cpp @@ -19,12 +19,11 @@ #include #include - -#include "fds/thread_vector.hpp" -#include "stream_tracker.hpp" - #include +#include "sisl/fds/thread_vector.hpp" +#include "sisl/fds/stream_tracker.hpp" + using namespace sisl; SISL_LOGGING_INIT(test_stream_tracker) @@ -33,6 +32,8 @@ namespace { struct TestData { TestData(int val) : m_value(val) {} int m_value = 0; + + bool operator==(const TestData& other) const { return (m_value == other.m_value); } }; struct StreamTrackerTest : public testing::Test { @@ -53,7 +54,7 @@ struct StreamTrackerTest : public testing::Test { StreamTrackerTest() {} size_t get_mem_size() { auto json = MetricsFarm::getInstance().get_result_in_json(); - return (size_t)json["StreamTracker"]["StreamTracker"]["Gauges"]["Total Memsize for stream tracker"]; + return (size_t)json["StreamTracker"]["StreamTracker_2"]["Gauges"]["Total Memsize for stream tracker"]; } }; } // namespace @@ -61,7 +62,7 @@ struct StreamTrackerTest : public testing::Test { TEST_F(StreamTrackerTest, SimpleCompletions) { static std::random_device s_rd{}; static std::default_random_engine s_engine{s_rd()}; - std::uniform_int_distribution< int64_t > gen{0, 999}; + std::uniform_int_distribution< int > gen{0, 999}; for (auto i = 0; i < 100; ++i) { m_tracker.create_and_complete(i, gen(s_engine)); @@ -97,7 +98,7 @@ TEST_F(StreamTrackerTest, SimpleCompletions) { TEST_F(StreamTrackerTest, ForceRealloc) { static std::random_device s_rd{}; static std::default_random_engine s_engine{s_rd()}; - std::uniform_int_distribution< int64_t > gen{0, 999}; + std::uniform_int_distribution< int > gen{0, 999}; auto prev_size = get_mem_size(); auto far_idx = (int64_t)StreamTracker< TestData >::alloc_blk_size + 1; @@ -109,7 +110,60 @@ TEST_F(StreamTrackerTest, ForceRealloc) { m_tracker.create_and_complete(i, gen(s_engine)); } EXPECT_EQ(m_tracker.completed_upto(), far_idx); - EXPECT_EQ(get_mem_size(), prev_size); + EXPECT_EQ(get_mem_size(), prev_size * 2); +} + +TEST_F(StreamTrackerTest, Rollback) { + static std::random_device s_rd{}; + static std::default_random_engine s_engine{s_rd()}; + std::uniform_int_distribution< int > gen{0, 999}; + + for (auto i = 0; i < 200; ++i) { + m_tracker.create(i, gen(s_engine)); + } + EXPECT_EQ(m_tracker.active_upto(), 199); + EXPECT_EQ(m_tracker.completed_upto(), -1); + m_tracker.complete(0, 99); + EXPECT_EQ(m_tracker.active_upto(), 199); + EXPECT_EQ(m_tracker.completed_upto(), 99); + + m_tracker.rollback(169); + EXPECT_EQ(m_tracker.active_upto(), 169); + EXPECT_EQ(m_tracker.completed_upto(), 99); + + m_tracker.complete(100, 169); + EXPECT_EQ(m_tracker.active_upto(), 169); + EXPECT_EQ(m_tracker.completed_upto(), 169); + + auto new_val1 = gen(s_engine); + auto new_val2 = gen(s_engine); + m_tracker.create(170, new_val1); + m_tracker.create(172, new_val2); + EXPECT_EQ(m_tracker.active_upto(), 170); + EXPECT_EQ(m_tracker.completed_upto(), 169); + m_tracker.complete(170, 170); + EXPECT_EQ(m_tracker.completed_upto(), 170); + m_tracker.create_and_complete(171, new_val2); + m_tracker.complete(172, 172); + + EXPECT_EQ(m_tracker.completed_upto(), 172); + EXPECT_EQ(m_tracker.at(170), TestData{new_val1}); + EXPECT_EQ(m_tracker.at(171), TestData{new_val2}); + EXPECT_EQ(m_tracker.at(172), TestData{new_val2}); + + bool exception_hit{false}; + m_tracker.truncate(80); + try { + m_tracker.rollback(1); + } catch (const std::out_of_range& e) { exception_hit = true; } + EXPECT_EQ(exception_hit, true); + + exception_hit = false; + m_tracker.truncate(173); + try { + m_tracker.rollback(1); + } catch (const std::out_of_range& e) { exception_hit = true; } + EXPECT_EQ(exception_hit, true); } int main(int argc, char* argv[]) { diff --git a/src/fds/tests/test_tcmalloc_helper.cpp b/src/fds/tests/test_tcmalloc_helper.cpp new file mode 100644 index 00000000..ad63453d --- /dev/null +++ b/src/fds/tests/test_tcmalloc_helper.cpp @@ -0,0 +1,92 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Bryan Zimmerman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#ifdef USING_TCMALLOC +#include +#include +#include +#include +#include + +#include "sisl/logging/logging.h" +#include "sisl/options/options.h" +#include "sisl/utility/thread_buffer.hpp" + +#include + +#include "sisl/fds/malloc_helper.hpp" + +using namespace sisl; + +SISL_LOGGING_INIT(test_jemalloc) + +namespace { +uint32_t g_num_threads; + +struct TcmallocTest : public testing::Test { +public: + TcmallocTest() : testing::Test{} { LOGINFO("Initializing new TcmallocTest class"); } + TcmallocTest(const TcmallocTest&) = delete; + TcmallocTest(TcmallocTest&&) noexcept = delete; + TcmallocTest& operator=(const TcmallocTest&) = delete; + TcmallocTest& operator=(TcmallocTest&&) noexcept = delete; + virtual ~TcmallocTest() override = default; + +protected: + void SetUp() override {} + void TearDown() override {} + + void MultiThreadedAllocDealloc(const size_t iterations, const size_t mem_count = 1000000) const { + const auto thread_lambda{[&iterations, &mem_count]() { + // allocated/deallocate memory + for (size_t iteration{0}; iteration < iterations; ++iteration) { + std::unique_ptr< uint64_t[] > mem{new uint64_t[mem_count]}; + } + }}; + + std::vector< std::thread > threads; + for (uint32_t thread_num{0}; thread_num < g_num_threads; ++thread_num) { + threads.emplace_back(thread_lambda); + } + + for (auto& alloc_dealloc_thread : threads) { + if (alloc_dealloc_thread.joinable()) alloc_dealloc_thread.join(); + }; + } +}; +} // namespace + +TEST_F(TcmallocTest, GetDirtyPageCount) { MultiThreadedAllocDealloc(100); } + +SISL_OPTIONS_ENABLE(logging, test_tcmalloc) + +SISL_OPTION_GROUP(test_tcmalloc, + (num_threads, "", "num_threads", "number of threads", + ::cxxopts::value< uint32_t >()->default_value("8"), "number")) + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging, test_tcmalloc); + ::testing::InitGoogleTest(&argc, argv); + sisl::logging::SetLogger("test_bitset"); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + g_num_threads = SISL_OPTIONS["num_threads"].as< uint32_t >(); + + const auto ret{RUN_ALL_TESTS()}; + return ret; +} + +#endif diff --git a/src/file_watcher/CMakeLists.txt b/src/file_watcher/CMakeLists.txt index 8efb5f48..aeaf9cd8 100644 --- a/src/file_watcher/CMakeLists.txt +++ b/src/file_watcher/CMakeLists.txt @@ -1,19 +1,18 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -add_flags("-Wno-unused-parameter") - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -set(FILE_WATCHER_SOURCE_FILES +add_library(sisl_file_watcher OBJECT) +target_sources(sisl_file_watcher PRIVATE file_watcher.cpp - ) -add_library(sisl_file_watcher OBJECT ${FILE_WATCHER_SOURCE_FILES}) + ) target_link_libraries(sisl_file_watcher ${COMMON_DEPS}) -set(TEST_FILE_WATCHER_SOURCES - file_watcher_test.cpp - ) -add_executable(test_file_watcher ${TEST_FILE_WATCHER_SOURCES}) -target_link_libraries(test_file_watcher sisl ${COMMON_DEPS} GTest::gtest GTest::gmock) -add_test(NAME test_file_watcher COMMAND test_file_watcher) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_file_watcher) + target_sources(test_file_watcher PRIVATE + file_watcher_test.cpp + ) + target_link_libraries(test_file_watcher sisl ${COMMON_DEPS} GTest::gtest GTest::gmock) + add_test(NAME FileWatcher COMMAND test_file_watcher) + endif() +endif() diff --git a/src/file_watcher/file_watcher.cpp b/src/file_watcher/file_watcher.cpp index 7bd8db42..94989052 100644 --- a/src/file_watcher/file_watcher.cpp +++ b/src/file_watcher/file_watcher.cpp @@ -6,8 +6,8 @@ #include #include -#include "file_watcher.hpp" -#include "utility/thread_factory.hpp" +#include "sisl/file_watcher/file_watcher.hpp" +#include "sisl/utility/thread_factory.hpp" namespace sisl { namespace fs = std::filesystem; @@ -69,7 +69,7 @@ bool FileWatcher::register_listener(const std::string& file_path, const std::str { auto lk = std::unique_lock< std::mutex >(m_files_lock); if (const auto it{m_files.find(file_path)}; it != m_files.end()) { - auto file_info = it->second; + auto& file_info = it->second; file_info.m_handlers.emplace(listener_id, file_event_handler); LOGDEBUG("File path {} exists, adding the handler cb for the listener {}", file_path, listener_id); return true; @@ -110,19 +110,26 @@ bool FileWatcher::unregister_listener(const std::string& file_path, const std::s file_info.m_handlers.erase(listener_id); if (file_info.m_handlers.empty()) { - if (auto err = inotify_rm_watch(m_inotify_fd, file_info.m_wd); err != 0) { - LOGERROR("inotify rm failed for file path {}, listener id {} errno: {}", file_path, listener_id, errno); + if (!remove_watcher(file_info)) { + LOGDEBUG("inotify rm failed for file path {}, listener id {} errno: {}", file_path, listener_id, errno); return false; } - m_files.erase(file_path); } return true; } +bool FileWatcher::remove_watcher(FileInfo& file_info) { + bool success = true; + if (auto err = inotify_rm_watch(m_inotify_fd, file_info.m_wd); err != 0) { success = false; } + // remove the file from the map regardless of the inotify_rm_watch result + m_files.erase(file_info.m_filepath); + return success; +} + bool FileWatcher::stop() { // signal event loop to break and wait for the thread to join // event value does not matter, this is just generating an event at the read end of the pipe - LOGINFO("Stopping file watcher event loop."); + LOGDEBUG("Stopping file watcher event loop."); int event = 1; int ret; do { @@ -134,7 +141,7 @@ bool FileWatcher::stop() { return false; } - LOGINFO("Waiting for file watcher thread to join.."); + LOGDEBUG("Waiting for file watcher thread to join.."); if (m_fw_thread && m_fw_thread->joinable()) { try { m_fw_thread->join(); @@ -143,7 +150,7 @@ bool FileWatcher::stop() { return false; } } - LOGINFO("file watcher thread joined."); + LOGINFO("file watcher stopped."); return true; } @@ -186,16 +193,18 @@ void FileWatcher::handle_events() { } void FileWatcher::on_modified_event(const int wd, const bool is_deleted) { - auto lk = std::unique_lock< std::mutex >(m_files_lock); FileInfo file_info; get_fileinfo(wd, file_info); if (is_deleted) { + // There is a corner case (very unlikely) where a new listener + // regestered for this filepath after the current delete event was triggered. + { + auto lk = std::unique_lock< std::mutex >(m_files_lock); + remove_watcher(file_info); + } for (const auto& [id, handler] : file_info.m_handlers) { handler(file_info.m_filepath, true); } - // There is a corner case (very unlikely) where a new listener - // regestered for this filepath after the current delete event was triggered. - m_files.erase(file_info.m_filepath); return; } @@ -243,8 +252,8 @@ bool FileWatcher::get_file_contents(const std::string& file_name, std::string& c return false; } -// Hold the m_files_lock before calling this method. void FileWatcher::get_fileinfo(const int wd, FileInfo& file_info) const { + auto lk = std::unique_lock< std::mutex >(m_files_lock); for (const auto& [file_path, file] : m_files) { if (file.m_wd == wd) { file_info = file; diff --git a/src/file_watcher/file_watcher_test.cpp b/src/file_watcher/file_watcher_test.cpp index c5059958..9dbcb1e6 100644 --- a/src/file_watcher/file_watcher_test.cpp +++ b/src/file_watcher/file_watcher_test.cpp @@ -9,8 +9,8 @@ #include #include -#include "file_watcher.hpp" -#include "options/options.h" +#include "sisl/file_watcher/file_watcher.hpp" +#include SISL_LOGGING_INIT(test_file_watcher) SISL_OPTIONS_ENABLE(logging) @@ -21,55 +21,110 @@ using namespace ::testing; class FileWatcherTest : public ::testing::Test { public: - std::shared_ptr< FileWatcher > file_watcher; virtual void SetUp() override { - file_watcher = std::make_shared< FileWatcher >(); - EXPECT_TRUE(file_watcher->start()); + m_file_change_params.file_watcher = std::make_shared< FileWatcher >(); + EXPECT_TRUE(m_file_change_params.file_watcher->start()); } - virtual void TearDown() override { EXPECT_TRUE(file_watcher->stop()); } + virtual void TearDown() override { + EXPECT_TRUE(m_file_change_params.file_watcher->stop()); + std::remove(m_file_change_params.file_str.c_str()); + } - std::mutex file_change_lock; - std::condition_variable file_change_cv; + struct FileChangeParams { + std::shared_ptr< FileWatcher > file_watcher; + std::string file_str; + std::mutex file_change_lock; + std::condition_variable file_change_cv; + bool is_deleted; + int cb_call_count; + }; + FileChangeParams m_file_change_params; }; +void monitor_file_changes(FileWatcherTest::FileChangeParams& file_change_params, const std::string& listener) { + EXPECT_TRUE(file_change_params.file_watcher->register_listener( + file_change_params.file_str, listener, + [&file_change_params, listener](const std::string filepath, const bool deleted) { + EXPECT_EQ(file_change_params.file_str, filepath); + { + std::lock_guard< std::mutex > lg(file_change_params.file_change_lock); + file_change_params.is_deleted = deleted; + file_change_params.cb_call_count--; + } + if (deleted) { + std::ofstream file_of{file_change_params.file_str}; + monitor_file_changes(file_change_params, listener); + } + file_change_params.file_change_cv.notify_one(); + })); +} + TEST_F(FileWatcherTest, basic_watcher) { const auto file_path = fs::current_path() / "basic_test.txt"; // remove if exists and then create a new file fs::remove(file_path); - const std::string file_str{file_path.string()}; - std::ofstream file_of{file_str}; - bool is_deleted = true; - bool cb_called = false; - - EXPECT_TRUE(file_watcher->register_listener( - file_str, "basic_test_listener", - [this, &is_deleted, &cb_called, &file_str](const std::string filepath, const bool deleted) { - EXPECT_EQ(file_str, filepath); - { - std::lock_guard< std::mutex > lg(file_change_lock); - is_deleted = deleted; - cb_called = true; - } - file_change_cv.notify_one(); - })); + m_file_change_params.file_str = file_path.string(); + std::ofstream file_of{m_file_change_params.file_str}; + m_file_change_params.is_deleted = true; + m_file_change_params.cb_call_count = 1; + + monitor_file_changes(m_file_change_params, "basic_listener"); // edit the file file_of << "Hello World!"; file_of.flush(); { - auto lk = std::unique_lock< std::mutex >(file_change_lock); - EXPECT_TRUE(file_change_cv.wait_for(lk, std::chrono::milliseconds(500), [&cb_called]() { return cb_called; })); - EXPECT_FALSE(is_deleted); - cb_called = false; // set it false for the next iteration of the test + auto lk = std::unique_lock< std::mutex >(m_file_change_params.file_change_lock); + EXPECT_TRUE(m_file_change_params.file_change_cv.wait_for( + lk, std::chrono::milliseconds(1500), [this]() { return m_file_change_params.cb_call_count == 0; })); + EXPECT_FALSE(m_file_change_params.is_deleted); + m_file_change_params.cb_call_count = 1; // set it 1 for the next iteration of the test } // remove the file fs::remove(file_path); { - auto lk = std::unique_lock< std::mutex >(file_change_lock); - EXPECT_TRUE(file_change_cv.wait_for(lk, std::chrono::milliseconds(500), [&cb_called]() { return cb_called; })); - EXPECT_TRUE(is_deleted); + auto lk = std::unique_lock< std::mutex >(m_file_change_params.file_change_lock); + EXPECT_TRUE(m_file_change_params.file_change_cv.wait_for( + lk, std::chrono::milliseconds(1500), [this]() { return m_file_change_params.cb_call_count == 0; })); + EXPECT_TRUE(m_file_change_params.is_deleted); + m_file_change_params.cb_call_count = 1; // set it 1 for the next iteration of the test + } + + /* TODO fix this in CI. + std::ofstream file_of1{m_file_change_params.file_str}; + file_of1 << "Hello World Again!"; + file_of1.flush(); + { + auto lk = std::unique_lock< std::mutex >(m_file_change_params.file_change_lock); + EXPECT_TRUE(m_file_change_params.file_change_cv.wait_for( + lk, std::chrono::milliseconds(1500), [this]() { return m_file_change_params.cb_call_count == 0; })); + EXPECT_FALSE(m_file_change_params.is_deleted); + } + */ +} + +TEST_F(FileWatcherTest, multiple_watchers) { + const auto file_path = fs::current_path() / "basic_test.txt"; + // remove if exists and then create a new file + fs::remove(file_path); + m_file_change_params.file_str = file_path.string(); + std::ofstream file_of{m_file_change_params.file_str}; + m_file_change_params.is_deleted = true; + m_file_change_params.cb_call_count = 2; + + monitor_file_changes(m_file_change_params, "basic_listener1"); + monitor_file_changes(m_file_change_params, "basic_listener2"); + + // edit the file + file_of << "Hello World!"; + file_of.flush(); + { + auto lk = std::unique_lock< std::mutex >(m_file_change_params.file_change_lock); + EXPECT_TRUE(m_file_change_params.file_change_cv.wait_for( + lk, std::chrono::milliseconds(1500), [this]() { return m_file_change_params.cb_call_count == 0; })); + EXPECT_FALSE(m_file_change_params.is_deleted); } } diff --git a/src/flip/CMakeLists.txt b/src/flip/CMakeLists.txt new file mode 100644 index 00000000..368cabce --- /dev/null +++ b/src/flip/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.11) + +find_package(gRPC QUIET REQUIRED) + +add_subdirectory (proto) + +if(NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) +endif() +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(flip) +target_sources(flip PRIVATE + lib/flip_rpc_server.cpp + $ + ) +target_link_libraries(flip + sisl + gRPC::grpc++ + spdlog::spdlog + nlohmann_json::nlohmann_json + ) + +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_flip) + target_sources(test_flip PRIVATE + lib/test_flip.cpp + ) + target_link_libraries(test_flip flip cxxopts::cxxopts) + add_test(NAME Flip COMMAND test_flip) + + add_executable(test_flip_server) + target_sources(test_flip_server PRIVATE + lib/test_flip_server.cpp + ) + target_link_libraries(test_flip_server flip cxxopts::cxxopts) + + add_executable(test_flip_local_client) + target_sources(test_flip_local_client PRIVATE + client/local/test_flip_local_client.cpp + ) + target_link_libraries(test_flip_local_client flip cxxopts::cxxopts) + add_test(NAME FlipLocalClient COMMAND test_flip_local_client) + endif() +endif() diff --git a/src/flip/README.md b/src/flip/README.md new file mode 100644 index 00000000..ecfb6b95 --- /dev/null +++ b/src/flip/README.md @@ -0,0 +1,350 @@ +# Flip + +Flip stands for **F**au**l**t **I**njection **P**oint. Its a generic framework for injecting fault into the code. +It provides a framework, where actual fault could be injected outside the application. + +# Fault Injection + +To induce the system to take an alternate path of the code, we simulate faults. One typical way to achieve +this is by explicitly writing a injected fault and make it compile time option to trigger, something like. + +```c++ +io_status IO::submit(packet_t *pkt) { + ... +#ifdef INJECT_SUBMIT_FAULT + return io_status::error; +#endif + .... + return real_submit(pkt); +} +``` + +Unfortunately this is extremely limited because every time a fault needs to triggered it needs to be recompiled. Another +limitation to this approach is that it hits on every call to a method irrespective of any condition. This makes it lot of +hand holding and affect automation of the test cases. Flip tries to eliminate these issues and provide a generic framework +to ease out fault injection. + +Following are some of the important features of Flip: + +* **Multiple fault injection points:** Flip supports multiple fault injection points in a single program. +Each fault injection point is associated with an unique name. Example: + +```c++ +Flip flip; +io_status IO::submit(packet_t *pkt) { + if (flip.test_flip("fail_all_writes")) { + // Do your failure generation here + return io_status::error; + } + return real_submit(pkt); +} +``` + +* **Trigger fault externally:** One of the major design goal is the ability to trigger these faults externally. Flip provides a +protobuf based serialization, which can be used by any RPC mechanism. + +* **Parameterized faults:** In the above example, if there should be a provision for different types of packets to be injected at +different instances, there would be a fault injection for every type. This will quickly become unscalabale approach as more and +more packet types could be added and generic framework idea will be lost. Flip provides parameterized definition of faults. The +above example could be extended to + +```c++ +Flip flip; +io_status IO::submit(packet_t *pkt) { + if (flip.test_flip("fail_specific_writes", pkt->op_code)) { + // Do your failure generation here + return io_status::error; + } + return real_submit(pkt); +} +``` + +Here the _pkt->op_code_ is the parameter which could be controlled externally in-order to inject the fault. Flip supports filtering +various conditions (not just ==) to the value of the parameter. Hence, while triggering in the above example one can trigger +all OP_TYPE_CREATE or anything but OP_TYPE_CREATE etc. + +There are no limits to number of parameters, but the 2 conditions will be anded. The above example could be expanded to +```c++ +Flip flip; +io_status IO::submit(packet_t *pkt) { + if (flip.test_flip("fail_specific_writes", pkt->op_code, pkt->size)) { + // Do your failure generation here + return io_status::error; + } + return real_submit(pkt); +} +``` + +* **Return specific values:** It is useful to inject an alternate path, but what will be more useful is whats the error it should +generate as configurable. This will avoid multiple flip points for different types of error generation. Extending above example, +if one wants to simulate return of different errors from IO::submit, one can write similar _flip.test_flip("")_ for all possible +error codes, but it will become too verbose. Flip supports parameterized return as well. Hence it one could do the following +```c++ +Flip flip; +io_status IO::submit(packet_t *pkt) { + auto ret = flip.get_test_flip("fail_specific_writes", pkt->op_code, pkt->size); + if (ret) { + // Do your failure generation here + return ret.get(); + } + return real_submit(pkt); +} +``` + +Now fault injection externally can make flip return specific errors, not just io_status::error, but say io_status::corrupted_data +etc.. + +* **Async delay injection:** One more typical use of fault injection is to delay execution or simulate delays to test timeout code +paths etc.. In a sync code, it is easy to put a sleep to reproduce the delay. However, more and more applications do async operation +or code being completely async. In these cases, there needs a timer routine to keep track of the delay. Flip covers this and +creates a simple async delay injection framework. + +```c++ +Flip flip; +void IO::process_response(packet_t *pkt) { + if (flip.delay_flip("delay_response", [this, pkt]() { + IO::real_process_response(pkt); + }, pkt->op_code)) { + return; + } + IO::real_process_response(pkt); +``` + +Above example, provide the fault injection to delay specific opcode. After the configured delay (externally controllable) it calls +the closure. As always number of parameters are unlimited and it is exactly similar to the other types of fault injections explained above. +Also like other faults, it can be controlled externally on how many times and how frequent the faults have to be triggered. + +Flip supports combining delay_flip and simulated_value generation flip, so it can generate a specific value after imparting +delay. This will be useful, since after delay an application typically return a timeout error or other types of errors which +will have different behavior in apps, which needed to be tested. + +```c++ +Flip flip; +void IO::process_response(packet_t *pkt) { + if (flip.get_delay_flip("delay_response", [this, pkt](io_status generated_error) { + pkt->status = generated_error; + IO::real_process_response(pkt); + }, pkt->op_code)) { + return; + } + IO::real_process_response(pkt); +``` + +In the above example after delay the value injected externally is passed to the closure, which could be used to simulate +various error scenarios. + +# How to use Flip + +Flip usage has 2 phases + +* **Definition phase:** This is the phase where the declaration of which place and what action needs to be taken in application code +with the fault. It needs to be written before compiling the application code. +* **Injection phase:** This is the phase where the faults are injected or triggered either through local flip client or external client. + +There are **4** important parameters that needs to be determined for a fault point. The proto file _proto/flip_spec.proto_ defines +these parameters: + +**Flip name**: Unique name identifies this flip point. There can be multiple instances (with different parameters) for same flip +point, say "fail_writes" flip for opcode=1, opcode=2 can coexist (with the same name) in above examples. The name needs to be +declared during definition phase and addressed with that during injection phase. + +**Flip Parameters**: The list of parameters that needs to filter out a flip. During definition phase, application code needs to +decide what are the possible filtering attributes to control with. Thus it is advisable to write a flip in possible common portion +of the code and let injection phase decide what it needs to filter on. In above example **_flip.get_test_flip("fail_specific_writes", pkt->op_code, pkt->size)_** +allows the injection phase to filter on opcode and pkt_size. Note that if there are multiple parameters each filter conditions are and'd. + +During injection phase, user can supply values and operator for each parameter. Flip as of now only supports primitive types (int, long, +double, bool, uint64) and string, const char* as parameter types. It supports all operators (==, >, <, >=, <=, !=) and one more called "*" +to ignore the check for this parameter. + +**Flip Action**: If the fault is triggered what action the application should take. Flip supports 4 types of action + +* **No explicit action**: Flip does not take any other action other than returning the fault is hit. Application code will then +write the error simulation code. +* **Return a value action**: A value decided during injection phase (of type determined during definition phase) will be returned +as part of flip hit. +* **Delay action**: Introduce a time delay determined during the injection phase. +* **Delay and return a value action**: Combining above 2. + +**Flip frequency**: This is actioned only during injection phase, which determines how frequently and how much the flip has to hit +or trigger the fault. + +Count: How many times it needs to hit. +Frequency: + either percentage of times it needs to hit (to randomly hit for this much percentage) or + every nth time + +## Flip APIs +Flip can be initialized with default constructor. In future it will accept parameters like having application own timer routine +and also specific grpc instance. As of now flip when called with default constructor creates/uses its own timer and thread for +delay and does not provide any RPC service to call. + +### test_flip +```c++ +template< class... Args > +bool test_flip(std::string flip_name, Args &&... args); +``` +Test if flip is triggered or not. Parameters are +* flip_name: Name of the flip +* args: variable list of arguments to filter. Arguments can be of primitive types or std::string or const char * + +Returns: If flip is hit or not. Flip is hit only if it matches the filter criteria and frequency of injection criteria. This API +can be called on any of the 4 types of flip. + +### get_test_flip +```c++ +template< typename T, class... Args > +boost::optional< T > get_test_flip(std::string flip_name, Args &&... args); +``` +Test if flip is triggered and if triggered, returns injected value. +* flip_name: Name of the flip +* args: variable list of arguments to filter. Arguments can be of primitive types or std::string or const char * + +Returns: If flip is not hit, returns boost::none, otherwise returns the injected value. The injected value cannot be of one of the +primitive types or std::string. This API is only valid for "return a value action" flip type. + +### delay_flip +```c++ +template< class... Args > +bool delay_flip(std::string flip_name, const std::function &closure, Args &&... args); +``` +Test if flip is triggered and if triggered, calls the supplied closure callback after injected delay time in microseconds. +* flip_name: Name of the flip +* closure: The callback closure to call after the delay, if flip is hit +* args: variable list of arguments to filter. Arguments can be of primitive types or std::string or const char * + +Returns: If the flip is hit or not. Whether flip is hit or not is immediately known. + +### get_delay_flip +```c++ +template +bool get_delay_flip(std::string flip_name, const std::function &closure, Args &&... args); +``` +Test if flip is triggered and if triggered, calls the supplied closure callback after injected delay time in microseconds with +the injected value as a parameter. +* flip_name: Name of the flip +* closure: The callback closure to call after the delay, if flip is hit. The closure should accept the parameter of type which +the fault could be injected with. +* args: variable list of arguments to filter. Arguments can be of primitive types or std::string or const char * + +Returns: If the flip is hit or not. Whether flip is hit or not is immediately known. + +## Integration with Application + +Flip is a header only framework and hence will be included and compiled along with application binary. It uses a protobuf to +serialize the message about how faults can be triggered. The protobuf could be used against any RPCs the application provide. + +Flip supports an optional GRPC server which can be started using + +```c++ +Flip::start_rpc_server() +``` + +If application uses GRPC and if another GRPC server creation is to be avoided, then application can instead add the +following grpc definition to its RPC call to the grpc service proto. +```c++ +// Inject a fault rpc +rpc InjectFault (flip.FlipSpec) returns (flip.FlipResponse); +``` + +# Flip Client + +Flip needs a client to trigger the faults externally. At present there are 2 forms of flip client, one GRPC client, which +allows flip faults can be injected remotely from external application. Second form is local flip client, which means +flip will be triggered by the same application binary which is to be fault tested. + +## GRPC Client + +### Python GRPC Client +There is python grpc client library through which it can be triggered remotely. To setup the python client, execute +``` +bash ./setup_python_client.sh +``` + +Python library is available under ***src/client/python/flip_rpc_client.py*** + +Libraries are defined in class **FlipRPCClient**. Examples of how to use library is provided by the python script +***src/client/python/flip_client_example.py*** + +### Nodejs GRPC Client + +There is a current implementation using GRPC for a project called "NuData/MonstorDB.git" which has nodejs client to inject the fault. Example of +grpc service is provided in path "MonstorDB/nodejs-test/test/support/monstor_client/inject_fault.js" and examples of how to use is +in "MonstorDB/nodejst-test/test/support/run_grpc_client.js" + +Example: +```javascript +await test.do_inject_fault( + "op_error_in_bson", + [{name : "op_type", oper : FlipOperator.EQUAL.ordinal, value : {string_value : "INSERT"} }], // Conditions} + { returns : { return : { int_value : 6 } } }, // Returns BSON_DOCUMENT_CORRUPTED + 1, // count + 100 // percentage +) +``` + + +## Local Client +If the code that needs to be fault injected and tested is a library in itself and that there is separate unit tests which runs +the code, we don't need an RPC, but local calls to trigger the faults. For example, if the tested code runs with GTest or other +unit test framework, the test code runs in the same context as actual code. Flip provides a FlipClient class to trigger faults. + +Following are the APIs for FlipClient +### FlipClient() +```c++ +FlipClient(Flip *f) +``` +Constructs a flipclient providing the flip instances of actual code. Typically FlipClient and Flip could be singleton instances. + +### create_condition +```c++ +template< typename T> +void create_condition(const std::string& param_name, flip::Operator oper, const T& value, FlipCondition *out_condition); +``` +Parameters are: +* param_name: This is not used for any cross verification but just for logging purposes +* oper: One of the flip operators (==, >, <, >=, <= !=, *) +* value: Value parameter of the filter criteria +* condition: Returns the FlipCondition that will be passed in subsequent APIs. + +### Inject APIs +```c++ +bool inject_noreturn_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, const FlipFrequency &freq); + +template +bool inject_retval_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, const T& retval); + +bool inject_delay_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency& freq, uint64_t delay_usec); +template +bool inject_delay_and_retval_flip(std::string flip_name, const std::vector< FlipCondition >& conditions, + const FlipFrequency &freq, uint64_t delay_usec, const T& retval); +``` + +Parameters are: +* flip_name: Name of the flip to inject the fault to +* conditions: Vector of conditions which will be and'd. Each condition can be created using create_condition API +* freq: Flip frequency determines how much and how frequent. Can be constructed using _FlipFrequency::set_count(), +FlipFrequency::set_percent(), FlipFrequency::set_every_nth()_ +* retval: (for _inject_retval_flip_ and _inject_delay_and_retval_flip_): What is the injected value to be returned or called back respecitvely +* delay_usec: (for _inject_delay_flip_ and _inject_delay_and_retval_flip_): How much delay to inject + +Returns: +* If successfully injected the fault or not. + +Example code +```c++ + Flip flip; + FlipClient fclient(&flip); + ... + FlipCondition cond1, cond2; + fclient.create_condition("cmd_type", flip::Operator::EQUAL, (int)1, &cond1); + fclient.create_condition("size_bytes", flip::Operator::LESS_THAN_OR_EQUAL, (long)2048, &cond2); + + FlipFrequency freq; + freq.set_count(2); freq.set_percent(100); + fclient.inject_delay_flip("delay_flip", {cond1, cond2}, freq, 100000 /* delay in usec */); +``` +Above examples, trigger a flip called delay flip to inject a delay of 100ms if cmd_type == 1 and size_bytes <= 2048 + diff --git a/src/flip/client/local/test_flip_local_client.cpp b/src/flip/client/local/test_flip_local_client.cpp new file mode 100644 index 00000000..a98e79a2 --- /dev/null +++ b/src/flip/client/local/test_flip_local_client.cpp @@ -0,0 +1,197 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include "proto/flip_spec.pb.h" +#include "sisl/flip/flip_client.hpp" +#include +#include + +#include +#include + +using namespace flip; + +SISL_LOGGING_INIT(flip) +SISL_OPTIONS_ENABLE(logging) + +Flip g_flip; + +void run_and_validate_noret_flip() { + int valid_cmd = 1; + int invalid_cmd = -1; + + RELEASE_ASSERT(!g_flip.test_flip("noret_flip", invalid_cmd), "notret_flip invalid cmd succeeeded - unexpected"); + RELEASE_ASSERT(g_flip.test_flip("noret_flip", valid_cmd), "notret_flip valid cmd failed - unexpected"); + RELEASE_ASSERT(!g_flip.test_flip("noret_flip", invalid_cmd), "notret_flip valid cmd succeeeded - unexpected"); + RELEASE_ASSERT(g_flip.test_flip("noret_flip", valid_cmd), "notret_flip valid cmd failed - unexpected"); + RELEASE_ASSERT(!g_flip.test_flip("noret_flip", valid_cmd), + "notret_flip valid cmd succeeeded - no more than 2 expected to succeed"); // Not more than 2 +} + +void run_and_validate_ret_flip() { + std::string my_vol = "vol1"; + std::string valid_dev_name = "/dev/sda"; + std::string unknown_vol = "unknown_vol"; + std::string invalid_dev_name = "/boot/sda"; + + auto result = g_flip.get_test_flip< std::string >("simval_flip", my_vol, valid_dev_name); + RELEASE_ASSERT(result, "get_test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT_EQ(result.get(), "Simulated error value", "Incorrect flip returned"); + + result = g_flip.get_test_flip< std::string >("simval_flip", unknown_vol, valid_dev_name); + RELEASE_ASSERT(!result, "get_test_flip succeeded for incorrect conditions - unexpected"); + + result = g_flip.get_test_flip< std::string >("simval_flip", my_vol, invalid_dev_name); + RELEASE_ASSERT(!result, "get_test_flip succeeded for incorrect conditions - unexpected"); + + result = g_flip.get_test_flip< std::string >("simval_flip", my_vol, valid_dev_name); + RELEASE_ASSERT(result, "get_test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT_EQ(result.get(), "Simulated error value", "Incorrect flip returned"); + + result = g_flip.get_test_flip< std::string >("simval_flip", my_vol, valid_dev_name); + RELEASE_ASSERT(!result, "get_test_flip freq set to 2, but 3rd time hit as well - unexpected"); // Not more than 2 +} + +void run_and_validate_delay_flip() { + int valid_cmd = 1; + long valid_size_bytes1 = 2047; + long valid_size_bytes2 = 2048; + int invalid_cmd = -1; + long invalid_size_bytes = 4096; + std::shared_ptr< std::atomic< int > > closure_calls = std::make_shared< std::atomic< int > >(0); + + RELEASE_ASSERT(g_flip.delay_flip( + "delay_flip", [closure_calls]() { (*closure_calls)++; }, valid_cmd, valid_size_bytes1), + "delay_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT(!g_flip.delay_flip( + "delay_flip", [closure_calls]() { (*closure_calls)++; }, invalid_cmd, valid_size_bytes1), + "delay_flip succeeded for invalid conditions - unexpected"); + RELEASE_ASSERT(g_flip.delay_flip( + "delay_flip", [closure_calls]() { (*closure_calls)++; }, valid_cmd, valid_size_bytes2), + "delay_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT(!g_flip.delay_flip( + "delay_flip", [closure_calls]() { (*closure_calls)++; }, valid_cmd, invalid_size_bytes), + "delay_flip succeeded for invalid conditions - unexpected"); + RELEASE_ASSERT(!g_flip.delay_flip( + "delay_flip", [closure_calls]() { (*closure_calls)++; }, valid_cmd, valid_size_bytes1), + "delay_flip hit more than the frequency set - unexpected"); + + sleep(2); + RELEASE_ASSERT_EQ((*closure_calls).load(), 2, "Not all delay flips hit are called back"); +} + +void run_and_validate_delay_return_flip() { + double valid_double = 2.0; + double invalid_double = 1.85; + std::shared_ptr< std::atomic< int > > closure_calls = std::make_shared< std::atomic< int > >(0); + + RELEASE_ASSERT(g_flip.get_delay_flip< std::string >( + "delay_simval_flip", + [closure_calls](std::string error) { + (*closure_calls)++; + RELEASE_ASSERT_EQ(error, "Simulated delayed errval", "Invalid closure called"); + }, + valid_double), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!g_flip.get_delay_flip< std::string >( + "delay_simval_flip", + [closure_calls](std::string) { + RELEASE_ASSERT(false, "Invalid closure called"); + (*closure_calls)++; + }, + invalid_double), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(g_flip.get_delay_flip< std::string >( + "delay_simval_flip", + [closure_calls](std::string error) { + RELEASE_ASSERT_EQ(error, "Simulated delayed errval", "Invalid closure called"); + (*closure_calls)++; + }, + valid_double), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!g_flip.get_delay_flip< std::string >( + "delay_simval_flip", + [closure_calls](std::string) { + RELEASE_ASSERT(false, "Invalid closure called"); + (*closure_calls)++; + }, + invalid_double), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(!g_flip.get_delay_flip< std::string >( + "delay_simval_flip", + [closure_calls](std::string error) { + RELEASE_ASSERT_EQ(error, "Simulated delayed errval", "Invalid closure called"); + (*closure_calls)++; + LOGINFO("Called with error = {}", error); + }, + valid_double), + "delay_flip hit more than the frequency set - unexpected"); + + sleep(2); + RELEASE_ASSERT_EQ((*closure_calls).load(), 2, "Not all delay flips hit are called back"); +} + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger(std::string(argv[0])); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + FlipClient fclient(&g_flip); + FlipFrequency freq; + + /* Inject a no return action flip */ + FlipCondition cond1; + fclient.create_condition("cmd_type", flip::Operator::EQUAL, (int)1, &cond1); + freq.set_count(2); + freq.set_percent(100); + fclient.inject_noreturn_flip("noret_flip", {cond1}, freq); + + /* Inject a invalid return action flip */ + FlipCondition cond2, cond6; + fclient.create_condition< std::string >("vol_name", flip::Operator::EQUAL, "vol1", &cond2); + fclient.create_condition< std::string >("dev_name", flip::Operator::REG_EX, "\\/dev\\/", &cond6); + freq.set_count(2); + freq.set_percent(100); + fclient.inject_retval_flip< std::string >("simval_flip", {cond2, cond6}, freq, "Simulated error value"); + + /* Inject a delay of 100ms action flip */ + FlipCondition cond3, cond4; + fclient.create_condition("cmd_type", flip::Operator::EQUAL, (int)1, &cond3); + fclient.create_condition("size_bytes", flip::Operator::LESS_THAN_OR_EQUAL, (long)2048, &cond4); + freq.set_count(2); + freq.set_percent(100); + fclient.inject_delay_flip("delay_flip", {cond3, cond4}, freq, 100000); + + /* Inject a delay of 1second and return a value action flip */ + FlipCondition cond5; + fclient.create_condition("double_val", flip::Operator::NOT_EQUAL, (double)1.85, &cond5); + freq.set_count(2); + freq.set_percent(100); + fclient.inject_delay_and_retval_flip< std::string >("delay_simval_flip", {cond5}, freq, 1000000, + "Simulated delayed errval"); + + /* Now execute the flip and validate that they are correct */ + run_and_validate_noret_flip(); + run_and_validate_ret_flip(); + run_and_validate_delay_flip(); + run_and_validate_delay_return_flip(); + + return 0; +} diff --git a/src/flip/client/python/flip_client_example.py b/src/flip/client/python/flip_client_example.py new file mode 100644 index 00000000..95f5288d --- /dev/null +++ b/src/flip/client/python/flip_client_example.py @@ -0,0 +1,49 @@ +from __future__ import print_function + +import random +import logging +from flip_rpc_client import * + +if __name__ == '__main__': + logging.basicConfig() + fclient = FlipRPCClient('localhost:50051') + + fclient.inject_test_flip("flip1", + fspec.FlipFrequency(count=4, percent=100), + [ + fspec.FlipCondition(oper=fspec.Operator.NOT_EQUAL, value=fspec.ParamValue(int_value=5)), + fspec.FlipCondition(oper=fspec.Operator.DONT_CARE) + ]) + fclient.inject_test_flip("flip2", + fspec.FlipFrequency(count=2, every_nth=5), + []) + + fclient.inject_ret_flip("flip3", + fspec.FlipFrequency(count=2, percent=100), + [ + fspec.FlipCondition(oper=fspec.Operator.NOT_EQUAL, value=fspec.ParamValue(int_value=5)), + ], + fspec.ParamValue(string_value="Simulated corruption") + ) + + fclient.inject_delay_flip("flip4", + fspec.FlipFrequency(count=10000, percent=100), + [ + fspec.FlipCondition(oper=fspec.Operator.GREATER_THAN_OR_EQUAL, + value=fspec.ParamValue(long_value=50000)), + ], + 1000 + ) + + fclient.inject_delay_ret_flip("flip5", + fspec.FlipFrequency(count=1, percent=50), + [ + fspec.FlipCondition(oper=fspec.Operator.LESS_THAN_OR_EQUAL, + value=fspec.ParamValue(double_value=800.15)), + ], + 1000, + fspec.ParamValue(bool_value=False) + ) + + fclient.flip_details("flip2") + fclient.all_flip_details() \ No newline at end of file diff --git a/src/flip/client/python/flip_rpc_client.py b/src/flip/client/python/flip_rpc_client.py new file mode 100644 index 00000000..91ba637c --- /dev/null +++ b/src/flip/client/python/flip_rpc_client.py @@ -0,0 +1,49 @@ +from __future__ import print_function + +import random +import logging +import sys + +import grpc +import flip_spec_pb2 as fspec +import flip_spec_pb2_grpc +import flip_server_pb2 +import flip_server_pb2_grpc + +class FlipRPCClient: + def __init__(self, server_address): + self.channel = grpc.insecure_channel(server_address) + self.stub = flip_server_pb2_grpc.FlipServerStub(self.channel) + + def inject_fault(self, name, freq, conds, action): + self.stub.InjectFault(fspec.FlipSpec(flip_name=name, conditions=conds, flip_action=action, flip_frequency=freq)) + + def inject_test_flip(self, name, freq, conds): + print("------ Inject test flip", name, "-------------") + self.inject_fault(name, freq, conds, fspec.FlipAction(no_action=1)) + + def inject_ret_flip(self, name, freq, conds, retval): + print("------ Inject ret flip", name, "-------------") + self.inject_fault(name, freq, conds, fspec.FlipAction(returns=fspec.FlipAction.ActionReturns(retval=retval))) + + def inject_delay_flip(self, name, freq, conds, delay_usec): + print("------ Inject delay flip", name, "-------------") + self.inject_fault(name, freq, conds, + fspec.FlipAction(delays=fspec.FlipAction.ActionDelays(delay_in_usec=delay_usec))) + + def inject_delay_ret_flip(self, name, freq, conds, delay_usec, retval): + print("------ Inject delay and then ret flip", name, "-------------") + self.inject_fault(name, freq, conds, + fspec.FlipAction(delay_returns=fspec.FlipAction.ActionDelayedReturns( + delay_in_usec=delay_usec, + retval=retval))) + + def flip_details(self, name): + list_response = self.stub.GetFaults(flip_server_pb2.FlipNameRequest(name=name)) + for finfo in list_response.infos: + print(finfo.info) + + def all_flip_details(self): + list_response = self.stub.GetFaults(flip_server_pb2.FlipNameRequest(name=None)) + for finfo in list_response.infos: + print(finfo.info) diff --git a/src/flip/client/python/setup_python_client.sh b/src/flip/client/python/setup_python_client.sh new file mode 100755 index 00000000..b3ff5386 --- /dev/null +++ b/src/flip/client/python/setup_python_client.sh @@ -0,0 +1,2 @@ +pip install grpcio-tools +python3 -m grpc_tools.protoc -I proto/ --python_out=src/client/python/gen_src --grpc_python_out=src/client/python/gen_src/ proto/*.proto diff --git a/src/flip/lib/flip_rpc_server.cpp b/src/flip/lib/flip_rpc_server.cpp new file mode 100644 index 00000000..769cb506 --- /dev/null +++ b/src/flip/lib/flip_rpc_server.cpp @@ -0,0 +1,66 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include + +#include +#include +#include +#include +#include + +#include "sisl/flip/flip_rpc_server.hpp" +#include "sisl/flip/flip.hpp" + +SISL_LOGGING_DEF(flip) + +namespace flip { +grpc::Status FlipRPCServer::InjectFault(grpc::ServerContext*, const FlipSpec* request, FlipResponse* response) { + LOGTRACEMOD(flip, "InjectFault request = {}", request->DebugString()); + flip::Flip::instance().add(*request); + response->set_success(true); + return grpc::Status::OK; +} + +grpc::Status FlipRPCServer::GetFaults(grpc::ServerContext*, const FlipNameRequest* request, + FlipListResponse* response) { + LOGTRACEMOD(flip, "GetFaults request = {}", request->DebugString()); + auto resp = request->name().size() ? flip::Flip::instance().get(request->name()) : flip::Flip::instance().get_all(); + for (const auto& r : resp) { + response->add_infos()->set_info(r); + } + LOGTRACEMOD(flip, "GetFaults response = {}", response->DebugString()); + return grpc::Status::OK; +} + +grpc::Status FlipRPCServer::RemoveFault(grpc::ServerContext*, const FlipRemoveRequest* request, + FlipRemoveResponse* response) { + LOGTRACEMOD(flip, "RemoveFault request = {}", request->DebugString()); + response->set_num_removed(flip::Flip::instance().remove(request->name())); + return grpc::Status::OK; +} + +class FlipRPCServiceWrapper : public FlipRPCServer::Service { +public: + void print_method_names() { + for (auto i = 0; i < 2; ++i) { + auto method = (::grpc::internal::RpcServiceMethod*)GetHandler(i); + if (method) { LOGINFOMOD(flip, "Method name = {}", method->name()); } + } + } +}; + +} // namespace flip diff --git a/src/flip/lib/test_flip.cpp b/src/flip/lib/test_flip.cpp new file mode 100644 index 00000000..a151f518 --- /dev/null +++ b/src/flip/lib/test_flip.cpp @@ -0,0 +1,257 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include "proto/flip_spec.pb.h" +#include "sisl/flip/flip.hpp" +#include +#include + +#include + +SISL_LOGGING_INIT(flip) +SISL_OPTIONS_ENABLE(logging) + +void create_ret_fspec(flip::FlipSpec* fspec) { + *(fspec->mutable_flip_name()) = "ret_fspec"; + + // Create a new condition and add it to flip spec + auto cond = fspec->mutable_conditions()->Add(); + *cond->mutable_name() = "coll_name"; + cond->set_oper(flip::Operator::EQUAL); + cond->mutable_value()->set_string_value("item_shipping"); + + fspec->mutable_flip_action()->mutable_returns()->mutable_retval()->set_string_value("Error simulated value"); + + auto freq = fspec->mutable_flip_frequency(); + freq->set_count(2); + freq->set_percent(100); +} + +void run_and_validate_ret_flip(flip::Flip* flip) { + std::string my_coll = "item_shipping"; + std::string unknown_coll = "unknown_collection"; + + auto result = flip->get_test_flip< std::string >("ret_fspec", my_coll); + RELEASE_ASSERT(result, "get_test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT_EQ(result.get(), "Error simulated value", "Incorrect flip returned"); + + result = flip->get_test_flip< std::string >("ret_fspec", unknown_coll); + RELEASE_ASSERT(!result, "get_test_flip succeeded for incorrect conditions - unexpected"); + + result = flip->get_test_flip< std::string >("ret_fspec", my_coll); + RELEASE_ASSERT(result, "get_test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT_EQ(result.get(), "Error simulated value", "Incorrect flip returned"); + + result = flip->get_test_flip< std::string >("ret_fspec", my_coll); + RELEASE_ASSERT(!result, "get_test_flip freq set to 2, but 3rd time hit as well - unexpected"); // Not more than 2 +} + +void create_check_fspec(flip::FlipSpec* fspec) { + *(fspec->mutable_flip_name()) = "check_fspec"; + + auto cond = fspec->mutable_conditions()->Add(); + *cond->mutable_name() = "cmd_type"; + cond->set_oper(flip::Operator::EQUAL); + cond->mutable_value()->set_int_value(1); + + auto freq = fspec->mutable_flip_frequency(); + freq->set_count(2); + freq->set_percent(100); +} + +void run_and_validate_check_flip(flip::Flip* flip) { + int valid_cmd = 1; + int invalid_cmd = -1; + + RELEASE_ASSERT(!flip->test_flip("check_fspec", invalid_cmd), + "test_flip succeeded for incorrect conditions - unexpected"); + RELEASE_ASSERT(flip->test_flip("check_fspec", valid_cmd), "test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT(!flip->test_flip("check_fspec", invalid_cmd), + "test_flip succeeded for incorrect conditions - unexpected"); + RELEASE_ASSERT(flip->test_flip("check_fspec", valid_cmd), "test_flip failed for valid conditions - unexpected"); + RELEASE_ASSERT(!flip->test_flip("check_fspec", valid_cmd), + "test_flip freq set to 2, but 3rd time hit as well - unexpected"); // Not more than 2 +} + +void create_delay_fspec(flip::FlipSpec* fspec) { + *(fspec->mutable_flip_name()) = "delay_fspec"; + + auto cond = fspec->mutable_conditions()->Add(); + *cond->mutable_name() = "cmd_type"; + cond->set_oper(flip::Operator::EQUAL); + cond->mutable_value()->set_int_value(2); + + fspec->mutable_flip_action()->mutable_delays()->set_delay_in_usec(100000); + auto freq = fspec->mutable_flip_frequency(); + freq->set_count(2); + freq->set_percent(100); +} + +void run_and_validate_delay_flip(flip::Flip* flip) { + int valid_cmd = 2; + int invalid_cmd = -1; + std::shared_ptr< std::atomic< int > > closure_calls = std::make_shared< std::atomic< int > >(0); + + RELEASE_ASSERT(flip->delay_flip( + "delay_fspec", [closure_calls]() { (*closure_calls)++; }, valid_cmd), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!flip->delay_flip( + "delay_fspec", [closure_calls]() { (*closure_calls)++; }, invalid_cmd), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(flip->delay_flip( + "delay_fspec", [closure_calls]() { (*closure_calls)++; }, valid_cmd), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!flip->delay_flip( + "delay_fspec", [closure_calls]() { (*closure_calls)++; }, invalid_cmd), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(!flip->delay_flip( + "delay_fspec", [closure_calls]() { (*closure_calls)++; }, valid_cmd), + "delay_flip hit more than the frequency set - unexpected"); + + sleep(2); + RELEASE_ASSERT_EQ((*closure_calls).load(), 2, "Not all delay flips hit are called back"); +} + +void create_delay_ret_fspec(flip::FlipSpec* fspec) { + *(fspec->mutable_flip_name()) = "delay_ret_fspec"; + + auto cond = fspec->mutable_conditions()->Add(); + *cond->mutable_name() = "cmd_type"; + cond->set_oper(flip::Operator::EQUAL); + cond->mutable_value()->set_int_value(2); + + fspec->mutable_flip_action()->mutable_delay_returns()->set_delay_in_usec(100000); + fspec->mutable_flip_action()->mutable_delay_returns()->mutable_retval()->set_string_value( + "Delayed error simulated value"); + + auto freq = fspec->mutable_flip_frequency(); + freq->set_count(2); + freq->set_percent(100); +} + +void run_and_validate_delay_return_flip(flip::Flip* flip) { + int valid_cmd = 2; + int invalid_cmd = -1; + std::shared_ptr< std::atomic< int > > closure_calls = std::make_shared< std::atomic< int > >(0); + + RELEASE_ASSERT(flip->get_delay_flip< std::string >( + "delay_ret_fspec", + [closure_calls]([[maybe_unused]] std::string error) { + (*closure_calls)++; + DEBUG_ASSERT_EQ(error, "Delayed error simulated value"); + }, + valid_cmd), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!flip->get_delay_flip< std::string >( + "delay_ret_fspec", + [closure_calls](std::string) { + assert(0); + (*closure_calls)++; + }, + invalid_cmd), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(flip->get_delay_flip< std::string >( + "delay_ret_fspec", + [closure_calls]([[maybe_unused]] std::string error) { + DEBUG_ASSERT_EQ(error, "Delayed error simulated value"); + (*closure_calls)++; + }, + valid_cmd), + "delay_flip failed for valid conditions - unexpected"); + + RELEASE_ASSERT(!flip->get_delay_flip< std::string >( + "delay_ret_fspec", + [closure_calls](std::string) { + assert(0); + (*closure_calls)++; + }, + invalid_cmd), + "delay_flip succeeded for invalid conditions - unexpected"); + + RELEASE_ASSERT(!flip->get_delay_flip< std::string >( + "delay_ret_fspec", + [closure_calls](std::string error) { + DEBUG_ASSERT_EQ(error, "Delayed error simulated value"); + (*closure_calls)++; + LOGINFO("Called with error = {}", error); + }, + valid_cmd), + "delay_flip hit more than the frequency set - unexpected"); + + sleep(2); + RELEASE_ASSERT_EQ((*closure_calls).load(), 2, "Not all delay flips hit are called back"); +} + +#if 0 +void create_multi_cond_fspec(flip::FlipSpec *fspec) { + *(fspec->mutable_flip_name()) = "multi_cond1_fspec"; + + // Create a new condition and add it to flip spec + auto cond1 = fspec->mutable_conditions()->Add(); + *cond1->mutable_name() = "cmd_type"; + cond1->set_oper(flip::Operator::EQUAL); + cond1->mutable_value()->set_int_value(1); + + auto cond2 = fspec->mutable_conditions()->Add(); + *cond2->mutable_name() = "coll_name"; + cond2->set_oper(flip::Operator::EQUAL); + cond2->mutable_value()->set_string_value("item_shipping"); + + fspec->mutable_flip_action()->mutable_returns()->mutable_return_()->set_string_value("Error simulated value"); + + auto freq = fspec->mutable_flip_frequency(); + freq->set_count(2); + freq->set_percent(100); +} +#endif + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger(std::string(argv[0])); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + flip::FlipSpec ret_fspec; + create_ret_fspec(&ret_fspec); + + flip::FlipSpec check_fspec; + create_check_fspec(&check_fspec); + + flip::FlipSpec delay_fspec; + create_delay_fspec(&delay_fspec); + + flip::FlipSpec delay_ret_fspec; + create_delay_ret_fspec(&delay_ret_fspec); + + flip::Flip flip; + flip.start_rpc_server(); + flip.add(ret_fspec); + flip.add(check_fspec); + flip.add(delay_fspec); + flip.add(delay_ret_fspec); + + run_and_validate_ret_flip(&flip); + run_and_validate_check_flip(&flip); + run_and_validate_delay_flip(&flip); + run_and_validate_delay_return_flip(&flip); + + return 0; +} diff --git a/src/flip/lib/test_flip_server.cpp b/src/flip/lib/test_flip_server.cpp new file mode 100644 index 00000000..9f395282 --- /dev/null +++ b/src/flip/lib/test_flip_server.cpp @@ -0,0 +1,35 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Author/Developer(s): Harihara Kadayam + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include "sisl/flip/flip.hpp" + +#include + +SISL_LOGGING_INIT(flip) + +SISL_OPTIONS_ENABLE(logging) + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger(std::string(argv[0])); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + flip::Flip f; + f.start_rpc_server(); + + sleep(1000); + return 0; +} diff --git a/src/flip/proto/CMakeLists.txt b/src/flip/proto/CMakeLists.txt new file mode 100644 index 00000000..7bdb9856 --- /dev/null +++ b/src/flip/proto/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.11) + +add_library(flip_proto OBJECT + flip_server.proto + flip_spec.proto + ) +protobuf_generate(LANGUAGE cpp TARGET flip_proto PROTOS flip_spec.proto) +protobuf_generate(LANGUAGE cpp TARGET flip_proto PROTOS flip_server.proto) +protobuf_generate( + TARGET flip_proto + LANGUAGE grpc + GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc + PLUGIN protoc-gen-grpc=$ +) +target_link_libraries(flip_proto + protobuf::libprotobuf + gRPC::grpc++ + ) + +add_custom_target(flip_py_proto ALL) +protobuf_generate(LANGUAGE python TARGET flip_py_proto PROTOS flip_server.proto) +protobuf_generate(LANGUAGE python TARGET flip_py_proto PROTOS flip_spec.proto) +protobuf_generate( + TARGET flip_py_proto + PROTOS flip_server.proto flip_spec.proto + LANGUAGE grpc + GENERATE_EXTENSIONS _pb2_grpc.py + PLUGIN protoc-gen-grpc=$) diff --git a/src/flip/proto/flip_server.proto b/src/flip/proto/flip_server.proto new file mode 100644 index 00000000..42ac0bd8 --- /dev/null +++ b/src/flip/proto/flip_server.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package flip; + +import "flip_spec.proto"; + +message FlipListResponse { + message FlipInfo { string info = 1; } + + repeated FlipInfo infos = 1; +} + +message FlipNameRequest { string name = 1; } + +message FlipRemoveRequest { string name = 1; } + +message FlipRemoveResponse { uint32 num_removed = 1; } + +service FlipServer { + // Inject a fault rpc + rpc InjectFault(flip.FlipSpec) returns (flip.FlipResponse); + + // Get details about one or all faults + rpc GetFaults(FlipNameRequest) returns (FlipListResponse); + + // Remove a fault added earlier + rpc RemoveFault(FlipRemoveRequest) returns (FlipRemoveResponse); +} \ No newline at end of file diff --git a/src/flip/proto/flip_spec.proto b/src/flip/proto/flip_spec.proto new file mode 100644 index 00000000..e191994f --- /dev/null +++ b/src/flip/proto/flip_spec.proto @@ -0,0 +1,118 @@ +syntax = "proto3"; + +package flip; + +option cc_enable_arenas = true; +enum Operator { + // Equal. + EQUAL = 0; + + // Inequality. + NOT_EQUAL = 1; + + // Less than. + LESS_THAN = 2; + + // Less than or equal. + LESS_THAN_OR_EQUAL = 3; + + // Greater than. + GREATER_THAN = 4; + + // Greater than or equal. + GREATER_THAN_OR_EQUAL = 5; + + // Don't care about + DONT_CARE = 6; + + // RegEx Pattern + REG_EX = 7; +} + +enum Frequency { + // Every time generate a fault + ALWAYS = 0; + + // Generate fault alternate attempts + ALTERNATE = 1; + + // Fault on uniform random basis + UNI_RANDOM = 2; +} + +message ParamValue { + // The kind of value. + oneof kind { + bool null_value = 1; + + int32 int_value = 2; + + int64 long_value = 3; + + // Represents a double value. + double double_value = 4; + + // Represents a string value. + string string_value = 6; + + // Represents a boolean value. + bool bool_value = 7; + + bytes binary_value = 10; + + //google.protobuf.Any struct_encoded = 22; + + //This is a wrapper object that contains a single attribute with reserved name `list` + //google.protobuf.Any list_encoded = 23; + } +} + +message FlipCondition { + string name = 1; + Operator oper = 2; + ParamValue value = 3; +} + +message FlipAction { + message ActionReturns { + ParamValue retval = 1; + } + + message ActionDelays { + uint64 delay_in_usec = 1; + } + + message ActionDelayedReturns { + uint64 delay_in_usec = 1; + ParamValue retval = 2; + } + + oneof action { + bool no_action = 1; + ActionReturns returns = 2; + ActionDelays delays = 3; + ActionDelayedReturns delay_returns = 4; + } +} + +message FlipFrequency { + uint32 count = 1; // How many faults to generate, Default no limit + + oneof frequency { + uint32 percent = 2; // Percentage of requests that matches the condition to generate fault + uint32 every_nth = 3; // Generate fault for every nth request + } +} + +message FlipSpec { + string flip_name = 1; + repeated FlipCondition conditions = 2; + FlipAction flip_action = 3; + FlipFrequency flip_frequency = 4; +} + +message FlipResponse { + bool success = 1; + + map metadata = 200; +} diff --git a/src/grpc/CMakeLists.txt b/src/grpc/CMakeLists.txt new file mode 100644 index 00000000..b10493dc --- /dev/null +++ b/src/grpc/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required (VERSION 3.11) + +find_package(flatbuffers QUIET REQUIRED) +find_package(gRPC QUIET REQUIRED) + +include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/../auth_manager) + +add_library(sisl_grpc OBJECT) +target_sources(sisl_grpc PRIVATE + rpc_server.cpp + rpc_client.cpp + ) +target_link_libraries(sisl_grpc + gRPC::grpc++ + flatbuffers::flatbuffers + folly::folly + ${COMMON_DEPS} + ) + +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_subdirectory(tests) + endif() +endif() diff --git a/src/grpc/rpc_client.cpp b/src/grpc/rpc_client.cpp new file mode 100644 index 00000000..c6f85439 --- /dev/null +++ b/src/grpc/rpc_client.cpp @@ -0,0 +1,175 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include "sisl/grpc/rpc_client.hpp" +#include "utils.hpp" + +namespace sisl { + +GrpcBaseClient::GrpcBaseClient(const std::string& server_addr, const std::string& target_domain, + const std::string& ssl_cert) : + GrpcBaseClient::GrpcBaseClient(server_addr, nullptr, target_domain, ssl_cert) {} + +GrpcBaseClient::GrpcBaseClient(const std::string& server_addr, + const std::shared_ptr< sisl::GrpcTokenClient >& token_client, + const std::string& target_domain, const std::string& ssl_cert) : + m_server_addr(server_addr), + m_target_domain(target_domain), + m_ssl_cert(ssl_cert), + m_token_client(token_client) {} + +void GrpcBaseClient::init() { + ::grpc::SslCredentialsOptions ssl_opts; + if (!m_ssl_cert.empty()) { + if (load_ssl_cert(m_ssl_cert, ssl_opts.pem_root_certs)) { + if (!m_target_domain.empty()) { + ::grpc::ChannelArguments channel_args; + channel_args.SetSslTargetNameOverride(m_target_domain); + m_channel = ::grpc::CreateCustomChannel(m_server_addr, ::grpc::SslCredentials(ssl_opts), channel_args); + } else { + m_channel = ::grpc::CreateChannel(m_server_addr, ::grpc::SslCredentials(ssl_opts)); + } + + } else { + throw std::runtime_error("Unable to load ssl certification for grpc client"); + } + } else { + m_channel = ::grpc::CreateChannel(m_server_addr, ::grpc::InsecureChannelCredentials()); + } +} + +bool GrpcBaseClient::load_ssl_cert(const std::string& ssl_cert, std::string& content) { + return get_file_contents(ssl_cert, content); +} + +bool GrpcBaseClient::is_connection_ready() const { + return (m_channel->GetState(true) == grpc_connectivity_state::GRPC_CHANNEL_READY); +} + +std::mutex GrpcAsyncClientWorker::s_workers_mtx; +std::unordered_map< std::string, GrpcAsyncClientWorker::UPtr > GrpcAsyncClientWorker::s_workers; + +GrpcAsyncClientWorker::~GrpcAsyncClientWorker() { shutdown(); } +void GrpcAsyncClientWorker::shutdown() { + if (m_state == ClientState::RUNNING) { + m_cq.Shutdown(); + m_state = ClientState::SHUTTING_DOWN; + + for (auto& thr : m_threads) { + thr.join(); + } + + m_state = ClientState::TERMINATED; + } + + return; +} + +void GrpcAsyncClientWorker::run(uint32_t num_threads) { + LOGMSG_ASSERT_EQ(ClientState::INIT, m_state); + + if (num_threads == 0) { throw(std::invalid_argument("Need atleast one worker thread")); } + for (uint32_t i = 0u; i < num_threads; ++i) { + m_threads.emplace_back(&GrpcAsyncClientWorker::client_loop, this); + } + + m_state = ClientState::RUNNING; +} + +void GrpcAsyncClientWorker::client_loop() { +#ifdef _POSIX_THREADS +#ifndef __APPLE__ + auto tname = std::string("grpc_client").substr(0, 15); + pthread_setname_np(pthread_self(), tname.c_str()); +#endif /* __APPLE__ */ +#endif /* _POSIX_THREADS */ + + void* tag; + bool ok = false; + while (m_cq.Next(&tag, &ok)) { + // For client-side unary call, `ok` is always true, even server is not running + auto cm = static_cast< ClientRpcDataAbstract* >(tag); + cm->handle_response(ok); + delete cm; + } +} + +void GrpcAsyncClientWorker::create_worker(const std::string& name, int num_threads) { + std::lock_guard< std::mutex > lock(s_workers_mtx); + if (s_workers.find(name) != s_workers.end()) { return; } + + auto worker = std::make_unique< GrpcAsyncClientWorker >(); + worker->run(num_threads); + s_workers.insert(std::make_pair(name, std::move(worker))); +} + +GrpcAsyncClientWorker* GrpcAsyncClientWorker::get_worker(const std::string& name) { + std::lock_guard< std::mutex > lock(s_workers_mtx); + auto it = s_workers.find(name); + if (it == s_workers.end()) { return nullptr; } + return it->second.get(); +} + +void GrpcAsyncClientWorker::shutdown_all() { + std::lock_guard< std::mutex > lock(s_workers_mtx); + for (auto& it : s_workers) { + it.second->shutdown(); + // release worker, the completion queue holds by it need to be destroyed before grpc lib internal object + // g_core_codegen_interface + it.second.reset(); + } + s_workers.clear(); +} + +void GrpcAsyncClient::GenericAsyncStub::prepare_and_send_unary_generic( + ClientRpcDataInternal< grpc::ByteBuffer, grpc::ByteBuffer >* data, const grpc::ByteBuffer& request, + const std::string& method, uint32_t deadline) { + data->set_deadline(deadline); + if (m_token_client) { data->add_metadata(m_token_client->get_auth_header_key(), m_token_client->get_token()); } + // Note that async unary RPCs don't post a CQ tag in call + data->m_generic_resp_reader_ptr = m_generic_stub->PrepareUnaryCall(&data->m_context, method, request, cq()); + data->m_generic_resp_reader_ptr->StartCall(); + // CQ tag posted here + data->m_generic_resp_reader_ptr->Finish(&data->reply(), &data->status(), (void*)data); +} + +void GrpcAsyncClient::GenericAsyncStub::call_unary(const grpc::ByteBuffer& request, const std::string& method, + const generic_unary_callback_t& callback, uint32_t deadline) { + auto data = new GenericClientRpcDataCallback(callback); + prepare_and_send_unary_generic(data, request, method, deadline); +} + +void GrpcAsyncClient::GenericAsyncStub::call_rpc(const generic_req_builder_cb_t& builder_cb, const std::string& method, + const generic_rpc_comp_cb_t& done_cb, uint32_t deadline) { + auto cd = new GenericClientRpcData(done_cb); + builder_cb(cd->m_req); + prepare_and_send_unary_generic(cd, cd->m_req, method, deadline); +} + +generic_async_result_t GrpcAsyncClient::GenericAsyncStub::call_unary(const grpc::ByteBuffer& request, + const std::string& method, uint32_t deadline) { + auto [p, sf] = folly::makePromiseContract< generic_result_t >(); + auto data = new GenericClientRpcDataFuture(std::move(p)); + prepare_and_send_unary_generic(data, request, method, deadline); + return std::move(sf); +} + +std::unique_ptr< GrpcAsyncClient::GenericAsyncStub > GrpcAsyncClient::make_generic_stub(const std::string& worker) { + auto w = GrpcAsyncClientWorker::get_worker(worker); + if (w == nullptr) { throw std::runtime_error("worker thread not available"); } + + return std::make_unique< GrpcAsyncClient::GenericAsyncStub >(std::make_unique< grpc::GenericStub >(m_channel), w, + m_token_client); +} +} // namespace sisl diff --git a/src/grpc/rpc_server.cpp b/src/grpc/rpc_server.cpp new file mode 100644 index 00000000..f23afc88 --- /dev/null +++ b/src/grpc/rpc_server.cpp @@ -0,0 +1,222 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include "sisl/grpc/rpc_server.hpp" +#include "sisl/grpc/generic_service.hpp" +#include "utils.hpp" + +#ifdef _POSIX_THREADS +#ifndef __APPLE__ +extern "C" { +#include +} +#endif +#endif + +#include + +SISL_LOGGING_DEF(grpc_server) + +namespace sisl { +GrpcServer::GrpcServer(const std::string& listen_addr, uint32_t threads, const std::string& ssl_key, + const std::string& ssl_cert) : + GrpcServer::GrpcServer(listen_addr, threads, ssl_key, ssl_cert, nullptr) {} + +GrpcServer::GrpcServer(const std::string& listen_addr, uint32_t threads, const std::string& ssl_key, + const std::string& ssl_cert, const std::shared_ptr< sisl::GrpcTokenVerifier >& auth_mgr) : + m_num_threads{threads}, m_auth_mgr{auth_mgr} { + if (listen_addr.empty() || threads == 0) { throw std::invalid_argument("Invalid parameter to start grpc server"); } + + if (!ssl_cert.empty() && !ssl_key.empty()) { + std::string key_contents; + std::string cert_contents; + + if (!get_file_contents(ssl_cert, cert_contents)) { + throw std::runtime_error("Unable to load ssl certification for grpc server"); + } + if (!get_file_contents(ssl_key, key_contents)) { + throw std::runtime_error("Unable to load ssl key for grpc server"); + } + + ::grpc::SslServerCredentialsOptions::PemKeyCertPair pkcp = {key_contents, cert_contents}; + ::grpc::SslServerCredentialsOptions ssl_opts; + ssl_opts.pem_root_certs = ""; + ssl_opts.pem_key_cert_pairs.push_back(pkcp); + + m_builder.AddListeningPort(listen_addr, ::grpc::SslServerCredentials(ssl_opts)); + } else { + m_builder.AddListeningPort(listen_addr, ::grpc::InsecureServerCredentials()); + } + + // Create one cq per thread + for (auto i = 0u; i < threads; ++i) { + m_cqs.emplace_back(m_builder.AddCompletionQueue()); + } + + m_state.store(ServerState::INITED); +} + +GrpcServer::~GrpcServer() { + shutdown(); + for (auto& [k, v] : m_services) { + (void)k; + delete v; + } +} + +GrpcServer* GrpcServer::make(const std::string& listen_addr, uint32_t threads, const std::string& ssl_key, + const std::string& ssl_cert) { + return GrpcServer::make(listen_addr, nullptr, threads, ssl_key, ssl_cert); +} + +GrpcServer* GrpcServer::make(const std::string& listen_addr, const std::shared_ptr< sisl::GrpcTokenVerifier >& auth_mgr, + uint32_t threads, const std::string& ssl_key, const std::string& ssl_cert) { + return new GrpcServer(listen_addr, threads, ssl_key, ssl_cert, auth_mgr); +} + +void GrpcServer::run(const rpc_thread_start_cb_t& thread_start_cb) { + LOGMSG_ASSERT_EQ(m_state.load(std::memory_order_relaxed), ServerState::INITED, "Grpcserver duplicate run?"); + + m_server = m_builder.BuildAndStart(); + + for (uint32_t i = 0; i < m_num_threads; ++i) { + auto t = std::make_shared< std::thread >(&GrpcServer::handle_rpcs, this, i, thread_start_cb); +#ifdef _POSIX_THREADS +#ifndef __APPLE__ + auto tname = std::string("grpc_server").substr(0, 15); + pthread_setname_np(t->native_handle(), tname.c_str()); +#endif /* __APPLE__ */ +#endif /* _POSIX_THREADS */ + m_threads.push_back(t); + } + + m_state.store(ServerState::RUNNING); +} + +void GrpcServer::handle_rpcs(uint32_t thread_num, const rpc_thread_start_cb_t& thread_start_cb) { + void* tag; + bool ok = false; + + if (thread_start_cb) { thread_start_cb(thread_num); } + + while (m_cqs[thread_num]->Next(&tag, &ok)) { + // `ok` is true if read a successful event, false otherwise. + [[likely]] if (tag != nullptr) { + // Process the rpc and refill the cq with a new rpc call + auto new_rpc_call = static_cast< RpcTag* >(tag)->process(ok); + if (new_rpc_call != nullptr) { new_rpc_call->enqueue_call_request(*m_cqs[new_rpc_call->m_queue_idx]); } + } + } +} + +void GrpcServer::shutdown() { + if (m_state.load() == ServerState::RUNNING) { + m_state.store(ServerState::SHUTTING_DOWN); + + m_server->Shutdown(); + for (auto& cq : m_cqs) { + cq->Shutdown(); // Always *after* the associated server's Shutdown()! + } + + m_server->Wait(); + // drain the cq_ + for (auto& thr : m_threads) { + if (thr->joinable()) thr->join(); + } + + m_state.store(ServerState::TERMINATED); + } +} + +bool GrpcServer::is_auth_enabled() const { return m_auth_mgr != nullptr; } + +grpc::Status GrpcServer::auth_verify(grpc::ServerContext const* srv_ctx) const { return m_auth_mgr->verify(srv_ctx); } + +bool GrpcServer::run_generic_handler_cb(const std::string& rpc_name, boost::intrusive_ptr< GenericRpcData >& rpc_data) { + generic_rpc_handler_cb_t cb; + { + std::shared_lock< std::shared_mutex > lock(m_generic_rpc_registry_mtx); + auto it = m_generic_rpc_registry.find(rpc_name); + if (it == m_generic_rpc_registry.end()) { + auto status{ + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, fmt::format("generic RPC {} not registered", rpc_name))}; + rpc_data->set_status(status); + // respond immediately + return true; + } + cb = it->second; + } + return cb(rpc_data); +} + +bool GrpcServer::register_async_generic_service() { + if (m_state.load() != ServerState::INITED) { + LOGMSG_ASSERT(false, "register service in non-INITED state"); + return false; + } + + if (m_generic_service_registered) { + LOGWARN("Duplicate register generic async service"); + return false; + } + m_generic_service = std::make_unique< grpc::AsyncGenericService >(); + m_builder.RegisterAsyncGenericService(m_generic_service.get()); + m_generic_rpc_static_info = std::make_unique< GenericRpcStaticInfo >(this, m_generic_service.get()); + m_generic_service_registered = true; + return true; +} + +bool GrpcServer::register_generic_rpc(const std::string& name, const generic_rpc_handler_cb_t& rpc_handler) { + if (m_state.load() != ServerState::RUNNING) { + LOGMSG_ASSERT(false, "register service in non-INITED state"); + return false; + } + + if (!m_generic_service_registered) { + LOGMSG_ASSERT(false, "RPC registration attempted before generic service is registered"); + return false; + } + + { + std::unique_lock< std::shared_mutex > lock(m_generic_rpc_registry_mtx); + if (auto [it, happened]{m_generic_rpc_registry.emplace(name, rpc_handler)}; !happened) { + LOGWARN("duplicate generic RPC {} registration attempted", name); + return false; + } + } + + // Register one call per cq. + for (auto i = 0u; i < m_cqs.size(); ++i) { + auto rpc_call = GenericRpcData::make(m_generic_rpc_static_info.get(), i); + rpc_call->enqueue_call_request(*m_cqs[i]); + } + return true; +} + +// RPCHelper static methods + +bool RPCHelper::has_server_shutdown(const GrpcServer* server) { + return (server->m_state.load(std::memory_order_acquire) != ServerState::RUNNING); +} + +bool RPCHelper::run_generic_handler_cb(GrpcServer* server, const std::string& method, + boost::intrusive_ptr< GenericRpcData >& rpc_data) { + return server->run_generic_handler_cb(method, rpc_data); +} + +grpc::Status RPCHelper::do_authorization(const GrpcServer* server, const grpc::ServerContext* srv_ctx) { + return (server->is_auth_enabled()) ? server->auth_verify(srv_ctx) : grpc::Status(); +} + +} // namespace sisl::grpc diff --git a/src/grpc/tests/CMakeLists.txt b/src/grpc/tests/CMakeLists.txt new file mode 100644 index 00000000..fc1ba245 --- /dev/null +++ b/src/grpc/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required (VERSION 3.11) + +add_subdirectory(proto) + +enable_testing() + +include_directories(BEFORE "${CMAKE_CURRENT_BINARY_DIR}/proto") + +add_subdirectory(function) +add_subdirectory(unit) diff --git a/src/grpc/tests/function/CMakeLists.txt b/src/grpc/tests/function/CMakeLists.txt new file mode 100644 index 00000000..50994d06 --- /dev/null +++ b/src/grpc/tests/function/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required (VERSION 3.11) + +# build echo_server +add_executable(echo_server) +target_sources(echo_server PRIVATE + echo_server.cpp + $ + ) +target_link_libraries(echo_server + sisl + sisl_grpc + GTest::gtest + ${COMMON_DEPS} + ) +add_test(NAME Echo_Ping_Server COMMAND echo_server) + +# build echo_async_client +add_executable(echo_async_client) +target_sources(echo_async_client PRIVATE + echo_async_client.cpp + $ + ) +target_link_libraries(echo_async_client + sisl + sisl_grpc + GTest::gtest + ${COMMON_DEPS} + ) +add_test(NAME Echo_Ping_Async_Client_Server COMMAND echo_async_client) diff --git a/src/grpc/tests/function/echo_async_client.cpp b/src/grpc/tests/function/echo_async_client.cpp new file mode 100644 index 00000000..57dba05b --- /dev/null +++ b/src/grpc/tests/function/echo_async_client.cpp @@ -0,0 +1,431 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "sisl/grpc/rpc_client.hpp" +#include "sisl/grpc/rpc_server.hpp" +#include "sisl/grpc/generic_service.hpp" +#include "grpc_helper_test.grpc.pb.h" + +using namespace sisl; +using namespace ::grpc_helper_test; +using namespace std::placeholders; + +struct DataMessage { + int m_seqno; + std::string m_buf; + + DataMessage() = default; + DataMessage(const int n, const std::string& buf) : m_seqno{n}, m_buf{buf} {} + + void SerializeToString(std::string& str_buf) const { + // first char denotes number of digits in seq_no + str_buf.append(std::to_string(numDigits(m_seqno))); + // append the seqno + str_buf.append(std::to_string(m_seqno)); + // append the data buffer + str_buf.append(m_buf); + } + void DeserializeFromString(const std::string& str_buf) { + int num_dig = str_buf[0] - '0'; + m_seqno = std::stoi(str_buf.substr(1, num_dig)); + m_buf = str_buf.substr(1 + num_dig); + } + + static int numDigits(int n) { + int ret = 0; + for (; n > 0; ret++) { + n /= 10; + } + return ret; + } +}; + +static void DeserializeFromBuffer(const grpc::ByteBuffer& buffer, DataMessage& msg) { + std::vector< grpc::Slice > slices; + (void)buffer.Dump(&slices); + std::string buf; + buf.reserve(buffer.Length()); + for (auto s = slices.begin(); s != slices.end(); s++) { + buf.append(reinterpret_cast< const char* >(s->begin()), s->size()); + } + msg.DeserializeFromString(buf); +} + +static void DeserializeFromBuffer(sisl::io_blob const& buffer, DataMessage& msg) { + std::string buf; + buf.reserve(buffer.size()); + buf.append(reinterpret_cast< const char* >(buffer.cbytes()), buffer.size()); + msg.DeserializeFromString(buf); +} + +static void SerializeToByteBuffer(grpc::ByteBuffer& buffer, const DataMessage& msg) { + std::string buf; + msg.SerializeToString(buf); + buffer.Clear(); + grpc::Slice slice(buf); + grpc::ByteBuffer tmp(&slice, 1); + buffer.Swap(&tmp); +} + +static const std::string GENERIC_CLIENT_MESSAGE{"I am a super client!"}; +static const std::string GENERIC_METHOD{"SendData"}; + +class TestClient { +public: + static constexpr int GRPC_CALL_COUNT = 400; + const std::string WORKER_NAME{"Worker-1"}; + + void validate_echo_reply(const EchoRequest& req, EchoReply& reply, ::grpc::Status const& status) { + RELEASE_ASSERT_EQ(status.ok(), true, "echo request {} failed, status {}: {}", req.message(), + status.error_code(), status.error_message()); + LOGDEBUGMOD(grpc_server, "echo request {} reply {}", req.message(), reply.message()); + RELEASE_ASSERT_EQ(req.message(), reply.message()); + { + std::unique_lock lk(m_wait_mtx); + if (--m_echo_counter == 0) { m_cv.notify_all(); } + } + } + + void validate_ping_reply(const PingRequest& req, PingReply& reply, ::grpc::Status const& status) { + RELEASE_ASSERT_EQ(status.ok(), true, "ping request {} failed, status {}: {}", req.seqno(), status.error_code(), + status.error_message()); + LOGDEBUGMOD(grpc_server, "ping request {} reply {}", req.seqno(), reply.seqno()); + RELEASE_ASSERT_EQ(req.seqno(), reply.seqno()); + { + std::unique_lock lk(m_wait_mtx); + if (--m_ping_counter == 0) { m_cv.notify_all(); } + } + } + + void validate_generic_reply(const DataMessage& req, grpc::ByteBuffer& reply, ::grpc::Status const& status) { + RELEASE_ASSERT_EQ(status.ok(), true, "generic request {} failed, status {}: {}", req.m_seqno, + status.error_code(), status.error_message()); + DataMessage svr_msg; + DeserializeFromBuffer(reply, svr_msg); + RELEASE_ASSERT_EQ(req.m_seqno, svr_msg.m_seqno); + RELEASE_ASSERT_EQ(req.m_buf, svr_msg.m_buf); + { + std::unique_lock lk(m_wait_mtx); + if (--m_generic_counter == 0) { m_cv.notify_all(); } + } + } + + void run(const std::string& server_address) { + auto client = std::make_unique< GrpcAsyncClient >(server_address, "", ""); + client->init(); + GrpcAsyncClientWorker::create_worker(WORKER_NAME, 4); + + auto echo_stub = client->make_stub< EchoService >(WORKER_NAME); + auto ping_stub = client->make_stub< PingService >(WORKER_NAME); + auto generic_stub = client->make_generic_stub(WORKER_NAME); + + m_echo_counter = static_cast< int >(GRPC_CALL_COUNT / 2); + // all numbers divisible by 3 but not 2 + m_ping_counter = static_cast< int >((GRPC_CALL_COUNT - 3) / 6) + 1; + m_generic_counter = GRPC_CALL_COUNT - m_echo_counter - m_ping_counter; + + for (int i = 1; i <= GRPC_CALL_COUNT; ++i) { + if ((i % 2) == 0) { + if ((i % 3) == 0) { + EchoRequest req; + req.set_message(std::to_string(i)); + echo_stub->call_unary< EchoRequest, EchoReply >( + req, &EchoService::StubInterface::AsyncEcho, + [req, this](EchoReply& reply, ::grpc::Status& status) { + validate_echo_reply(req, reply, status); + }, + 1); + } else if (i % 3 == 1) { + echo_stub->call_rpc< EchoRequest, EchoReply >( + [i](EchoRequest& req) { req.set_message(std::to_string(i)); }, + &EchoService::StubInterface::AsyncEcho, + [this](ClientRpcData< EchoRequest, EchoReply >& cd) { + validate_echo_reply(cd.req(), cd.reply(), cd.status()); + }, + 1); + } else { + EchoRequest req; + req.set_message(std::to_string(i)); + echo_stub->call_unary< EchoRequest, EchoReply >(req, &EchoService::StubInterface::AsyncEcho, 1) + .deferValue([req, this](auto e) { + RELEASE_ASSERT(e.hasValue(), "echo request {} failed, status {}: {}", req.message(), + e.error().error_code(), e.error().error_message()); + validate_echo_reply(req, e.value(), grpc::Status::OK); + return folly::Unit(); + }) + .get(); + } + } else if ((i % 3) == 0) { + // divide all numbers divisible by 3 and not by 2 into three equal buckets + auto const j = (i + 3) / 6; + if (j % 3 == 0) { + PingRequest req; + req.set_seqno(i); + ping_stub->call_unary< PingRequest, PingReply >( + req, &PingService::StubInterface::AsyncPing, + [req, this](PingReply& reply, ::grpc::Status& status) { + validate_ping_reply(req, reply, status); + }, + 1); + } else if (j % 3 == 1) { + ping_stub->call_rpc< PingRequest, PingReply >( + [i](PingRequest& req) { req.set_seqno(i); }, &PingService::StubInterface::AsyncPing, + [this](ClientRpcData< PingRequest, PingReply >& cd) { + validate_ping_reply(cd.req(), cd.reply(), cd.status()); + }, + 1); + } else { + PingRequest req; + req.set_seqno(i); + ping_stub->call_unary< PingRequest, PingReply >(req, &PingService::StubInterface::AsyncPing, 1) + .deferValue([req, this](auto e) { + RELEASE_ASSERT(e.hasValue(), "ping request {} failed, status {}: {}", req.seqno(), + e.error().error_code(), e.error().error_message()); + validate_ping_reply(req, e.value(), grpc::Status::OK); + return folly::Unit(); + }) + .get(); + } + } else { + // divide all numbers not divisible by 2 and 3 into three equal buckets + static uint32_t j = 0u; + if ((j++ % 3) == 0) { + DataMessage req(i, GENERIC_CLIENT_MESSAGE); + grpc::ByteBuffer cli_buf; + SerializeToByteBuffer(cli_buf, req); + generic_stub->call_unary( + cli_buf, GENERIC_METHOD, + [req, this](grpc::ByteBuffer& reply, ::grpc::Status& status) { + validate_generic_reply(req, reply, status); + }, + 1); + } else if (((j++ % 3) == 1)) { + DataMessage data_msg(i, GENERIC_CLIENT_MESSAGE); + generic_stub->call_rpc([data_msg](grpc::ByteBuffer& req) { SerializeToByteBuffer(req, data_msg); }, + GENERIC_METHOD, + [data_msg, this](GenericClientRpcData& cd) { + validate_generic_reply(data_msg, cd.reply(), cd.status()); + }, + 1); + } else { + DataMessage req(i, GENERIC_CLIENT_MESSAGE); + grpc::ByteBuffer cli_buf; + SerializeToByteBuffer(cli_buf, req); + generic_stub->call_unary(cli_buf, GENERIC_METHOD, 1) + .deferValue([req, this](auto e) { + RELEASE_ASSERT(e.hasValue(), "generic request {} failed, status {}: {}", req.m_seqno, + e.error().error_code(), e.error().error_message()); + validate_generic_reply(req, e.value(), grpc::Status::OK); + return folly::Unit(); + }) + .get(); + } + } + } + } + + void wait() { + std::unique_lock lk(m_wait_mtx); + m_cv.wait(lk, + [this]() { return ((m_echo_counter == 0) && (m_ping_counter == 0) && (m_generic_counter == 0)); }); + GrpcAsyncClientWorker::shutdown_all(); + } + +private: + int m_echo_counter; + int m_ping_counter; + int m_generic_counter; + std::mutex m_wait_mtx; + std::condition_variable m_cv; +}; + +class TestServer { +public: + class EchoServiceImpl final { + std::atomic< uint32_t > num_calls = 0ul; + + public: + ~EchoServiceImpl() = default; + + void register_service(GrpcServer* server) { + auto const res = server->register_async_service< EchoService >(); + RELEASE_ASSERT(res, "Failed to Register Service"); + } + + void register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + auto const res = server->register_rpc< EchoService, EchoRequest, EchoReply, false >( + "Echo", &EchoService::AsyncService::RequestEcho, + [this](const AsyncRpcDataPtr< EchoService, EchoRequest, EchoReply >& rpc_data) { + if ((++num_calls % 2) == 0) { + LOGDEBUGMOD(grpc_server, "respond async echo request {}", rpc_data->request().message()); + std::thread([rpc = rpc_data] { + rpc->response().set_message(rpc->request().message()); + rpc->send_response(); + }).detach(); + return false; + } + LOGDEBUGMOD(grpc_server, "respond sync echo request {}", rpc_data->request().message()); + rpc_data->response().set_message(rpc_data->request().message()); + return true; + }); + RELEASE_ASSERT(res, "register rpc failed"); + } + }; + + class PingServiceImpl final { + std::atomic< uint32_t > num_calls = 0ul; + + public: + ~PingServiceImpl() = default; + + void register_service(GrpcServer* server) { + auto const res = server->register_async_service< PingService >(); + RELEASE_ASSERT(res, "Failed to Register Service"); + } + + void register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + auto const res = server->register_rpc< PingService, PingRequest, PingReply, false >( + "Ping", &PingService::AsyncService::RequestPing, + [this](const AsyncRpcDataPtr< PingService, PingRequest, PingReply >& rpc_data) { + if ((++num_calls % 2) == 0) { + LOGDEBUGMOD(grpc_server, "respond async ping request {}", rpc_data->request().seqno()); + std::thread([rpc = rpc_data] { + rpc->response().set_seqno(rpc->request().seqno()); + rpc->send_response(); + }).detach(); + return false; + } + LOGDEBUGMOD(grpc_server, "respond sync ping request {}", rpc_data->request().seqno()); + rpc_data->response().set_seqno(rpc_data->request().seqno()); + return true; + }); + RELEASE_ASSERT(res, "register ping rpc failed"); + } + }; + + class GenericServiceImpl final { + std::atomic< uint32_t > num_calls = 0ul; + std::atomic< uint32_t > num_completions = 0ul; + + template < typename BufT > + static void set_response(BufT const& req, grpc::ByteBuffer& resp) { + DataMessage cli_request; + DeserializeFromBuffer(req, cli_request); + RELEASE_ASSERT((cli_request.m_buf == GENERIC_CLIENT_MESSAGE), "Could not parse response buffer"); + SerializeToByteBuffer(resp, cli_request); + } + + public: + void register_service(GrpcServer* server) { + auto const res = server->register_async_generic_service(); + RELEASE_ASSERT(res, "Failed to Register Service"); + } + + void register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + auto const res = + server->register_generic_rpc(GENERIC_METHOD, [this](boost::intrusive_ptr< GenericRpcData >& rpc_data) { + rpc_data->set_comp_cb([this](boost::intrusive_ptr< GenericRpcData >&) { num_completions++; }); + if ((++num_calls % 2) == 0) { + LOGDEBUGMOD(grpc_server, "respond async generic request, call_num {}", num_calls.load()); + std::thread([this, rpc = rpc_data] { + set_response(rpc->request_blob(), rpc->response()); + rpc->send_response(); + }).detach(); + return false; + } + set_response(rpc_data->request(), rpc_data->response()); + return true; + }); + RELEASE_ASSERT(res, "register generic rpc failed"); + } + + bool compare_counters() { + if (num_calls != num_completions) { + LOGERROR("num calls: {}, num_completions = {}", num_calls.load(), num_completions.load()); + return false; + } + return true; + } + }; + + void start(const std::string& server_address) { + LOGINFO("Start echo and ping server on {}...", server_address); + m_grpc_server = GrpcServer::make(server_address, 4, "", ""); + m_echo_impl = new EchoServiceImpl(); + m_echo_impl->register_service(m_grpc_server); + + m_ping_impl = new PingServiceImpl(); + m_ping_impl->register_service(m_grpc_server); + + m_generic_impl = new GenericServiceImpl(); + m_generic_impl->register_service(m_grpc_server); + + m_grpc_server->run(); + LOGINFO("Server listening on {}", server_address); + + m_echo_impl->register_rpcs(m_grpc_server); + m_ping_impl->register_rpcs(m_grpc_server); + m_generic_impl->register_rpcs(m_grpc_server); + } + + void shutdown() { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + RELEASE_ASSERT(m_generic_impl->compare_counters(), "num calls and num completions do not match!"); + LOGINFO("Shutting down grpc server"); + m_grpc_server->shutdown(); + delete m_grpc_server; + delete m_echo_impl; + delete m_ping_impl; + delete m_generic_impl; + } + +private: + GrpcServer* m_grpc_server = nullptr; + EchoServiceImpl* m_echo_impl = nullptr; + PingServiceImpl* m_ping_impl = nullptr; + GenericServiceImpl* m_generic_impl = nullptr; +}; + +SISL_LOGGING_INIT(logging, grpc_server) +SISL_OPTIONS_ENABLE(logging) + +int main(int argc, char** argv) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger("async_client"); + + TestServer server; + std::string server_address("0.0.0.0:50052"); + server.start(server_address); + + TestClient client; + client.run(server_address); + client.wait(); + + server.shutdown(); + return 0; +} diff --git a/src/grpc/tests/function/echo_server.cpp b/src/grpc/tests/function/echo_server.cpp new file mode 100644 index 00000000..f2a8290c --- /dev/null +++ b/src/grpc/tests/function/echo_server.cpp @@ -0,0 +1,148 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "sisl/grpc/rpc_server.hpp" +#include "grpc_helper_test.grpc.pb.h" + +using namespace ::grpc; +using namespace sisl; +using namespace ::grpc_helper_test; +using namespace std::placeholders; + +class EchoServiceImpl { +public: + virtual ~EchoServiceImpl() = default; + + virtual bool echo_request(const AsyncRpcDataPtr< EchoService, EchoRequest, EchoReply >& rpc_data) { + LOGINFO("receive echo request {}", rpc_data->request().message()); + rpc_data->response().set_message(rpc_data->request().message()); + return true; + } + + bool register_service(GrpcServer* server) { + if (!server->register_async_service< EchoService >()) { + LOGERROR("register service failed"); + return false; + } + + return true; + } + + bool register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + if (!server->register_rpc< EchoService, EchoRequest, EchoReply, false >( + "Echo", &EchoService::AsyncService::RequestEcho, std::bind(&EchoServiceImpl::echo_request, this, _1))) { + LOGERROR("register rpc failed"); + return false; + } + + return true; + } +}; + +class PingServiceImpl { + +public: + virtual ~PingServiceImpl() = default; + + virtual bool ping_request(const AsyncRpcDataPtr< PingService, PingRequest, PingReply >& rpc_data) { + LOGINFO("receive ping request {}", rpc_data->request().seqno()); + rpc_data->response().set_seqno(rpc_data->request().seqno()); + return true; + } + + bool register_service(GrpcServer* server) { + if (!server->register_async_service< PingService >()) { + LOGERROR("register ping service failed"); + return false; + } + return true; + } + + bool register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + if (!server->register_rpc< PingService, PingRequest, PingReply, false >( + "Ping", &PingService::AsyncService::RequestPing, std::bind(&PingServiceImpl::ping_request, this, _1))) { + LOGERROR("register ping rpc failed"); + return false; + } + + return true; + } +}; + +GrpcServer* g_grpc_server = nullptr; +EchoServiceImpl* g_echo_impl = nullptr; +PingServiceImpl* g_ping_impl = nullptr; + +void waiter_thread() { + std::this_thread::sleep_for(std::chrono::seconds(5)); + + LOGINFO("Shutting down grpc server"); + g_grpc_server->shutdown(); +} + +void StartServer() { + + std::string server_address("0.0.0.0:50051"); + + g_grpc_server = GrpcServer::make(server_address, 4, "", ""); + + g_echo_impl = new EchoServiceImpl(); + g_echo_impl->register_service(g_grpc_server); + + g_ping_impl = new PingServiceImpl(); + g_ping_impl->register_service(g_grpc_server); + + g_grpc_server->run(); + LOGINFO("Server listening on {}", server_address); + + g_echo_impl->register_rpcs(g_grpc_server); + g_ping_impl->register_rpcs(g_grpc_server); +} + +SISL_LOGGING_INIT(logging, grpc_server) +SISL_OPTIONS_ENABLE(logging) + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger("echo_server"); + LOGINFO("Start echo server ..."); + + StartServer(); + + auto t = std::thread(waiter_thread); + while (!g_grpc_server->is_terminated()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + t.join(); + delete g_grpc_server; + delete g_echo_impl; + delete g_ping_impl; + + return 0; +} diff --git a/src/grpc/tests/function/echo_sync_client.cpp b/src/grpc/tests/function/echo_sync_client.cpp new file mode 100644 index 00000000..8ccc9e86 --- /dev/null +++ b/src/grpc/tests/function/echo_sync_client.cpp @@ -0,0 +1,121 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "grpc_helper/rpc_client.hpp" +#include "sds_grpc_test.grpc.pb.h" + +using namespace ::grpc; +using namespace ::sisl; +using namespace ::sds_grpc_test; +using namespace std::placeholders; + +class EchoAndPingClient : public GrpcSyncClient { + +public: + using GrpcSyncClient::GrpcSyncClient; + + virtual void init() { + GrpcSyncClient::init(); + + echo_stub_ = MakeStub< EchoService >(); + ping_stub_ = MakeStub< PingService >(); + } + + const std::unique_ptr< EchoService::StubInterface >& echo_stub() { return echo_stub_; } + + const std::unique_ptr< PingService::StubInterface >& ping_stub() { return ping_stub_; } + +private: + std::unique_ptr< EchoService::StubInterface > echo_stub_; + std::unique_ptr< PingService::StubInterface > ping_stub_; +}; + +#define GRPC_CALL_COUNT 10 + +int RunClient(const std::string& server_address) { + + auto client = std::make_unique< EchoAndPingClient >(server_address, "", ""); + if (!client) { + LOGERROR("Create grpc sync client failed."); + return -1; + } + client->init(); + + int ret = 0; + for (int i = 0; i < GRPC_CALL_COUNT; i++) { + ClientContext context; + + if (i % 2 == 0) { + EchoRequest request; + EchoReply reply; + + request.set_message(std::to_string(i)); + Status status = client->echo_stub()->Echo(&context, request, &reply); + if (!status.ok()) { + LOGERROR("echo request {} failed, status {}: {}", request.message(), status.error_code(), + status.error_message()); + continue; + } + + LOGINFO("echo request {} reply {}", request.message(), reply.message()); + + if (request.message() == reply.message()) { ret++; } + } else { + PingRequest request; + PingReply reply; + + request.set_seqno(i); + Status status = client->ping_stub()->Ping(&context, request, &reply); + if (!status.ok()) { + LOGERROR("ping request {} failed, status {}: {}", request.seqno(), status.error_code(), + status.error_message()); + continue; + } + + LOGINFO("ping request {} reply {}", request.seqno(), reply.seqno()); + + if (request.seqno() == reply.seqno()) { ret++; } + } + } + + return ret; +} + +SISL_LOGGING_INIT() +SISL_OPTIONS_ENABLE(logging) + +int main(int argc, char** argv) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger("sync_client"); + + std::string server_address("0.0.0.0:50051"); + + if (RunClient(server_address) != GRPC_CALL_COUNT) { + LOGERROR("Only {} calls are successful", GRPC_CALL_COUNT); + return 1; + } + + return 0; +} diff --git a/src/grpc/tests/proto/CMakeLists.txt b/src/grpc/tests/proto/CMakeLists.txt new file mode 100644 index 00000000..1226be7b --- /dev/null +++ b/src/grpc/tests/proto/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.11) + +add_library(test_proto OBJECT) +target_sources(test_proto PRIVATE + grpc_helper_test.proto + ) +target_link_libraries(test_proto + gRPC::grpc++ + ) + +protobuf_generate(LANGUAGE cpp TARGET test_proto PROTOS) +protobuf_generate( + TARGET test_proto + LANGUAGE grpc + GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc + PLUGIN protoc-gen-grpc=$ +) diff --git a/src/grpc/tests/proto/grpc_helper_test.proto b/src/grpc/tests/proto/grpc_helper_test.proto new file mode 100644 index 00000000..8414feab --- /dev/null +++ b/src/grpc/tests/proto/grpc_helper_test.proto @@ -0,0 +1,41 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +syntax = "proto3"; + +package grpc_helper_test; + +service EchoService { + rpc Echo(EchoRequest) returns (EchoReply) {} + + rpc EchoMetadata(EchoRequest) returns (EchoReply) {} + + rpc EchoLongReply(EchoRequest) returns (stream EchoReply) {} + + rpc LongEcho(stream EchoRequest) returns (EchoReply) {} + + rpc LongEchoLongReply(stream EchoRequest) returns (stream EchoReply) {} +} + +message EchoRequest { string message = 1; } + +message EchoReply { string message = 1; } + +service PingService { + rpc Ping(PingRequest) returns (PingReply) {} +} + +message PingRequest { uint32 seqno = 1; } + +message PingReply { uint32 seqno = 1; } diff --git a/src/grpc/tests/unit/CMakeLists.txt b/src/grpc/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..b085b2bc --- /dev/null +++ b/src/grpc/tests/unit/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required (VERSION 3.10) + +add_executable(auth_test + auth_test.cpp + $ + ) +target_link_libraries(auth_test + sisl + sisl_grpc + GTest::gmock + ${COMMON_DEPS} + ) +add_test(NAME Auth_Test COMMAND auth_test) diff --git a/src/grpc/tests/unit/auth_test.cpp b/src/grpc/tests/unit/auth_test.cpp new file mode 100644 index 00000000..87720830 --- /dev/null +++ b/src/grpc/tests/unit/auth_test.cpp @@ -0,0 +1,422 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include + +#include +#include + +#include "sisl/grpc/rpc_client.hpp" +#include "sisl/grpc/rpc_server.hpp" +#include "grpc_helper_test.grpc.pb.h" + +SISL_LOGGING_INIT(logging, grpc_server) +SISL_OPTIONS_ENABLE(logging) + +namespace sisl::grpc::testing { +using namespace sisl; +using namespace ::grpc_helper_test; +using namespace ::testing; + +static const std::string grpc_server_addr{"0.0.0.0:12345"}; +static const std::string g_auth_header{"auth_header"}; +static const std::string g_test_token{"dummy_token"}; +static const std::string GENERIC_METHOD{"generic_method"}; + +static const std::vector< std::pair< std::string, std::string > > grpc_metadata{ + {sisl::request_id_header, "req_id1"}, {"key1", "val1"}, {"key2", "val2"}}; + +class EchoServiceImpl final { +public: + ~EchoServiceImpl() = default; + + bool echo_request(const AsyncRpcDataPtr< EchoService, EchoRequest, EchoReply >& rpc_data) { + LOGDEBUG("receive echo request {}", rpc_data->request().message()); + rpc_data->response().set_message(rpc_data->request().message()); + return true; + } + + bool echo_request_metadata(const AsyncRpcDataPtr< EchoService, EchoRequest, EchoReply >& rpc_data) { + LOGDEBUG("receive echo request {}", rpc_data->request().message()); + auto& client_headers = rpc_data->server_context().client_metadata(); + for (auto const& [key, val] : grpc_metadata) { + LOGINFO("metadata received, key = {}; val = {}", key, val) + auto const& it{client_headers.find(key)}; + if (it == client_headers.end()) { + rpc_data->set_status(::grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, ::grpc::string())); + } else if (it->second != val) { + LOGERROR("wrong value, expected = {}, actual = {}", val, it->second.data()) + rpc_data->set_status(::grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, ::grpc::string())); + } + } + return true; + } + + bool register_service(GrpcServer* server) { + if (!server->register_async_service< EchoService >()) { + LOGERROR("register service failed"); + return false; + } + + return true; + } + + bool register_rpcs(GrpcServer* server) { + LOGINFO("register rpc calls"); + if (!server->register_rpc< EchoService, EchoRequest, EchoReply, false >( + "Echo", &EchoService::AsyncService::RequestEcho, + std::bind(&EchoServiceImpl::echo_request, this, std::placeholders::_1))) { + LOGERROR("register rpc failed"); + return false; + } + if (!server->register_rpc< EchoService, EchoRequest, EchoReply, false >( + "EchoMetadata", &EchoService::AsyncService::RequestEchoMetadata, + std::bind(&EchoServiceImpl::echo_request_metadata, this, std::placeholders::_1))) { + LOGERROR("register rpc failed"); + return false; + } + return true; + } +}; + +class MockTokenVerifier : public GrpcTokenVerifier { +public: + using GrpcTokenVerifier::GrpcTokenVerifier; + ::grpc::Status verify(::grpc::ServerContext const* srv_ctx) const override { + auto& client_headers = srv_ctx->client_metadata(); + if (auto it = client_headers.find(g_auth_header); it != client_headers.end()) { + if (it->second == g_test_token) { return ::grpc::Status(); } + } + return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED, ::grpc::string("missing header authorization")); + } + + sisl::token_state_ptr verify(std::string const&) const override { + return std::make_shared< sisl::TokenVerifyState >(); + } +}; + +class AuthBaseTest : public ::testing::Test { +public: + void SetUp() override {} + + void TearDown() override { + if (m_grpc_server) { + m_grpc_server->shutdown(); + delete m_grpc_server; + delete m_echo_impl; + } + } + + void grpc_server_start(const std::string& server_address, std::shared_ptr< MockTokenVerifier > auth_mgr) { + LOGINFO("Start echo and ping server on {}...", server_address); + m_grpc_server = GrpcServer::make(server_address, auth_mgr, 4, "", ""); + m_echo_impl = new EchoServiceImpl(); + m_echo_impl->register_service(m_grpc_server); + m_grpc_server->register_async_generic_service(); + m_grpc_server->run(); + LOGINFO("Server listening on {}", server_address); + m_echo_impl->register_rpcs(m_grpc_server); + m_grpc_server->register_generic_rpc(GENERIC_METHOD, + [](boost::intrusive_ptr< GenericRpcData >&) { return true; }); + } + + void process_echo_reply() { + m_echo_received.store(true); + m_cv.notify_all(); + } + + void call_async_echo(EchoRequest& req, EchoReply& reply, ::grpc::Status& status) { + m_echo_stub->call_unary< EchoRequest, EchoReply >( + req, &EchoService::StubInterface::AsyncEcho, + [&reply, &status, this](EchoReply& reply_, ::grpc::Status& status_) { + reply = reply_; + status = status_; + process_echo_reply(); + }, + 1); + { + std::unique_lock lk(m_wait_mtx); + m_cv.wait(lk, [this]() { return m_echo_received.load(); }); + } + } + + void call_async_generic_rpc(::grpc::Status& status) { + ::grpc::ByteBuffer req; + m_generic_stub->call_unary( + req, GENERIC_METHOD, + [&status, this](::grpc::ByteBuffer&, ::grpc::Status& status_) { + status = status_; + m_generic_received.store(true); + m_cv.notify_all(); + }, + 1); + { + std::unique_lock lk(m_wait_mtx); + m_cv.wait(lk, [this]() { return m_generic_received.load(); }); + } + } + + void call_async_echo_metadata(EchoRequest& req, EchoReply& reply, ::grpc::Status& status) { + m_echo_stub->call_unary< EchoRequest, EchoReply >( + req, &EchoService::StubInterface::AsyncEchoMetadata, + [&reply, &status, this](EchoReply& reply_, ::grpc::Status& status_) { + reply = reply_; + status = status_; + process_echo_reply(); + }, + 1, grpc_metadata); + { + std::unique_lock lk(m_wait_mtx); + m_cv.wait(lk, [this]() { return m_echo_received.load(); }); + } + } + +protected: + std::shared_ptr< MockTokenVerifier > m_auth_mgr; + EchoServiceImpl* m_echo_impl = nullptr; + GrpcServer* m_grpc_server = nullptr; + std::unique_ptr< GrpcAsyncClient > m_async_grpc_client; + std::unique_ptr< GrpcAsyncClient::AsyncStub< EchoService > > m_echo_stub; + std::unique_ptr< GrpcAsyncClient::GenericAsyncStub > m_generic_stub; + std::atomic_bool m_echo_received{false}; + std::atomic_bool m_generic_received{false}; + std::mutex m_wait_mtx; + std::condition_variable m_cv; +}; + +class AuthDisableTest : public AuthBaseTest { +public: + void SetUp() override { + // start grpc server without auth + grpc_server_start(grpc_server_addr, nullptr); + + // Client without auth + m_async_grpc_client = std::make_unique< GrpcAsyncClient >(grpc_server_addr, "", ""); + m_async_grpc_client->init(); + GrpcAsyncClientWorker::create_worker("worker-1", 4); + m_echo_stub = m_async_grpc_client->make_stub< EchoService >("worker-1"); + m_generic_stub = m_async_grpc_client->make_generic_stub("worker-1"); + } + + void TearDown() override { AuthBaseTest::TearDown(); } +}; + +TEST_F(AuthDisableTest, allow_on_disabled_mode) { + EchoRequest req; + // server sets the same message as response + req.set_message("dummy_msg"); + EchoReply reply; + ::grpc::Status status; + call_async_echo(req, reply, status); + EXPECT_TRUE(status.ok()); + EXPECT_EQ(req.message(), reply.message()); + + ::grpc::Status generic_status; + call_async_generic_rpc(status); + EXPECT_TRUE(generic_status.ok()); +} + +TEST_F(AuthDisableTest, metadata) { + EchoRequest req; + EchoReply reply; + ::grpc::Status status; + call_async_echo_metadata(req, reply, status); + EXPECT_TRUE(status.ok()); +} + +class AuthServerOnlyTest : public AuthBaseTest { +public: + void SetUp() override { + // start grpc server with auth + m_auth_mgr = std::shared_ptr< MockTokenVerifier >(new MockTokenVerifier(g_auth_header)); + grpc_server_start(grpc_server_addr, m_auth_mgr); + + // Client without auth + m_async_grpc_client = std::make_unique< GrpcAsyncClient >(grpc_server_addr, "", ""); + m_async_grpc_client->init(); + GrpcAsyncClientWorker::create_worker("worker-2", 4); + m_echo_stub = m_async_grpc_client->make_stub< EchoService >("worker-2"); + m_generic_stub = m_async_grpc_client->make_generic_stub("worker-2"); + } + + void TearDown() override { AuthBaseTest::TearDown(); } +}; + +TEST_F(AuthServerOnlyTest, fail_on_no_client_auth) { + EchoRequest req; + // server sets the same message as response + req.set_message("dummy_msg"); + EchoReply reply; + ::grpc::Status status; + call_async_echo(req, reply, status); + EXPECT_FALSE(status.ok()); + EXPECT_EQ(status.error_code(), ::grpc::UNAUTHENTICATED); + EXPECT_EQ(status.error_message(), "missing header authorization"); + + ::grpc::Status generic_status; + call_async_generic_rpc(generic_status); + EXPECT_EQ(generic_status.error_code(), ::grpc::UNAUTHENTICATED); +} + +class MockGrpcTokenClient : public GrpcTokenClient { +public: + using GrpcTokenClient::GrpcTokenClient; + std::string get_token() override { return g_test_token; } +}; + +class AuthEnableTest : public AuthBaseTest { +public: + void SetUp() override { + // start grpc server with auth + m_auth_mgr = std::shared_ptr< MockTokenVerifier >(new MockTokenVerifier(g_auth_header)); + grpc_server_start(grpc_server_addr, m_auth_mgr); + + // Client with auth + m_token_client = std::make_shared< MockGrpcTokenClient >(g_auth_header); + m_async_grpc_client = std::make_unique< GrpcAsyncClient >(grpc_server_addr, m_token_client, "", ""); + m_async_grpc_client->init(); + GrpcAsyncClientWorker::create_worker("worker-3", 4); + m_echo_stub = m_async_grpc_client->make_stub< EchoService >("worker-3"); + m_generic_stub = m_async_grpc_client->make_generic_stub("worker-3"); + } + + void TearDown() override { AuthBaseTest::TearDown(); } + +protected: + std::shared_ptr< MockGrpcTokenClient > m_token_client; +}; + +TEST_F(AuthEnableTest, allow_with_auth) { + EchoRequest req; + req.set_message("dummy_msg"); + EchoReply reply; + ::grpc::Status status; + call_async_echo(req, reply, status); + EXPECT_TRUE(status.ok()); + EXPECT_EQ(req.message(), reply.message()); + + ::grpc::Status generic_status; + call_async_generic_rpc(status); + EXPECT_TRUE(generic_status.ok()); +} + +// sync client +class EchoAndPingClient : public GrpcSyncClient { + +public: + using GrpcSyncClient::GrpcSyncClient; + void init() override { + GrpcSyncClient::init(); + echo_stub_ = MakeStub< EchoService >(); + } + + const std::unique_ptr< EchoService::StubInterface >& echo_stub() { return echo_stub_; } + +private: + std::unique_ptr< EchoService::StubInterface > echo_stub_; +}; + +TEST_F(AuthEnableTest, allow_sync_client_with_auth) { + auto sync_client = std::make_unique< EchoAndPingClient >(grpc_server_addr, "", ""); + sync_client->init(); + EchoRequest req; + EchoReply reply; + req.set_message("dummy_sync_msg"); + ::grpc::ClientContext context; + context.AddMetadata(m_token_client->get_auth_header_key(), m_token_client->get_token()); + auto status = sync_client->echo_stub()->Echo(&context, req, &reply); + EXPECT_TRUE(status.ok()); + EXPECT_EQ(req.message(), reply.message()); +} + +void validate_generic_reply(const std::string& method, ::grpc::Status& status) { + if (method == "method1" || method == "method2") { + EXPECT_TRUE(status.ok()); + } else { + EXPECT_EQ(status.error_code(), ::grpc::UNIMPLEMENTED); + } +} + +TEST(GenericServiceDeathTest, basic_test) { + testing::GTEST_FLAG(death_test_style) = "threadsafe"; + auto g_grpc_server = GrpcServer::make("0.0.0.0:56789", nullptr, 1, "", ""); + // register rpc before generic service is registered +#ifndef NDEBUG + ASSERT_DEATH(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; }), + "Assertion .* failed"); +#else + EXPECT_FALSE(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; })); +#endif + + ASSERT_TRUE(g_grpc_server->register_async_generic_service()); + // duplicate register + EXPECT_FALSE(g_grpc_server->register_async_generic_service()); + // register rpc before server is run +#ifndef NDEBUG + ASSERT_DEATH(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; }), + "Assertion .* failed"); +#else + EXPECT_FALSE(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; })); +#endif + g_grpc_server->run(); + EXPECT_TRUE(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; })); + EXPECT_TRUE(g_grpc_server->register_generic_rpc( + "method2", [](boost::intrusive_ptr< GenericRpcData >&) { return true; })); + // re-register method 1 + EXPECT_FALSE(g_grpc_server->register_generic_rpc( + "method1", [](boost::intrusive_ptr< GenericRpcData >&) { return true; })); + + auto client = std::make_unique< GrpcAsyncClient >("0.0.0.0:56789", "", ""); + client->init(); + GrpcAsyncClientWorker::create_worker("generic_worker", 1); + auto generic_stub = client->make_generic_stub("generic_worker"); + ::grpc::ByteBuffer cli_buf; + generic_stub->call_unary( + cli_buf, "method1", + [method = "method1"](::grpc::ByteBuffer&, ::grpc::Status& status) { + validate_generic_reply(method, status); + }, + 1); + generic_stub->call_unary( + cli_buf, "method2", + [method = "method2"](::grpc::ByteBuffer&, ::grpc::Status& status) { + validate_generic_reply(method, status); + }, + 1); + generic_stub->call_unary( + cli_buf, "method_unknown", + [method = "method_unknown"](::grpc::ByteBuffer&, ::grpc::Status& status) { + validate_generic_reply(method, status); + }, + 1); +} + +} // namespace sisl::grpc::testing + +int main(int argc, char* argv[]) { + ::testing::InitGoogleMock(&argc, argv); + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger("auth_test"); + int ret{RUN_ALL_TESTS()}; + sisl::GrpcAsyncClientWorker::shutdown_all(); + return ret; +} diff --git a/src/grpc/utils.hpp b/src/grpc/utils.hpp new file mode 100644 index 00000000..1f4b4bc7 --- /dev/null +++ b/src/grpc/utils.hpp @@ -0,0 +1,32 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include + +namespace sisl { + +static bool get_file_contents(const std::string& file_name, std::string& contents) { + try { + std::ifstream f(file_name); + std::string buffer(std::istreambuf_iterator< char >{f}, std::istreambuf_iterator< char >{}); + contents = buffer; + return !contents.empty(); + } catch (...) {} + return false; +} + +} // namespace sisl::grpc diff --git a/src/logging/CMakeLists.txt b/src/logging/CMakeLists.txt index 680c2ce7..396f8994 100644 --- a/src/logging/CMakeLists.txt +++ b/src/logging/CMakeLists.txt @@ -1,22 +1,18 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-unused-parameter -Wno-cast-function-type") -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -set(LOGGING_SOURCE_FILES - lib/backtrace.cpp - lib/logging.cpp - lib/stacktrace.cpp - ) -add_library(sisl_logging OBJECT ${LOGGING_SOURCE_FILES}) +add_library(sisl_logging OBJECT) +target_sources(sisl_logging PRIVATE + logging.cpp + stacktrace.cpp + ) target_link_libraries(sisl_logging ${COMMON_DEPS}) -set(TEST_LOGGING_FILES - test/example.cpp - ) -add_executable(logging_example ${TEST_LOGGING_FILES}) -target_link_libraries(logging_example sisl ${COMMON_DEPS}) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(logging_example) + target_sources(logging_example PRIVATE + test/example.cpp + ) + target_link_libraries(logging_example sisl ${COMMON_DEPS}) + endif() +endif() diff --git a/src/logging/backtrace.h b/src/logging/backtrace.h deleted file mode 100644 index 4b654600..00000000 --- a/src/logging/backtrace.h +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (C) 2017-present Jung-Sang Ahn - * All rights reserved. - * - * https://github.com/greensky00 - * - * Stack Backtrace - * Version: 0.3.5 - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * =========================================================== - * - * Enhanced by hkadayam: - * - While dlsym is available, backtrace does not provide symbol name, fixed it - * by calculating the offset through dlsym. - */ - -#pragma once - -// LCOV_EXCL_START - -#include -#include -#include -#include -#include -#include -#include - -#if defined(__linux__) || defined(__APPLE__) -#include -#include -#endif - -#if defined(__linux__) -#include -#endif - -#ifdef __APPLE__ -#include -#endif - -namespace backtrace_detail { -constexpr size_t max_backtrace{256}; -constexpr size_t file_name_length{PATH_MAX}; -constexpr size_t symbol_name_length{1024}; -constexpr size_t address_length{16}; -constexpr uint64_t pipe_timeout_ms{15000}; // 15 seconds. Addr2line can be extremely slow the first time -} // namespace backtrace_detail - - -[[maybe_unused]] static size_t stack_backtrace_impl(void** const stack_ptr, const size_t stack_ptr_capacity) { - return ::backtrace(stack_ptr, static_cast< int >(stack_ptr_capacity)); -} - -#if defined(__linux__) -[[maybe_unused]] extern size_t stack_interpret_linux_file(const void* const* const stack_ptr, FILE* const stack_file, - const size_t stack_size, char* const output_buf, - const size_t output_buflen, const bool trim_internal); -#elif defined(__APPLE__) -[[maybe_unused]] extern size_t stack_interpret_apple(const void* const* const stack_ptr, - const char* const* const stack_msg, const size_t stack_size, - char* const output_buf, const size_t output_buflen, - const bool trim_internal, - [[maybe_unused]] const bool trim_internal); -#else -[[maybe_unused]] extern size_t stack_interpret_other(const void* const* const stack_ptr, - const char* const* const stack_msg, const size_t stack_size, - char* const output_buf, const size_t output_buflen, - [[maybe_unused]] const bool trim_internal); -#endif - -[[maybe_unused]] static size_t stack_interpret(void* const* const stack_ptr, const size_t stack_size, - char* const output_buf, const size_t output_buflen, - const bool trim_internal) { -#if defined(__linux__) - std::unique_ptr< FILE, std::function< void(FILE* const) > > stack_file{std::tmpfile(), [](FILE* const fp) { - if (fp) - std::fclose(fp); - }}; - if (!stack_file) - return 0; - - ::backtrace_symbols_fd(stack_ptr, static_cast< int >(stack_size), ::fileno(stack_file.get())); - - const size_t len{ - stack_interpret_linux_file(stack_ptr, stack_file.get(), stack_size, output_buf, output_buflen, trim_internal)}; -#else - const std::unique_ptr< char*, std::function< void(char** const) > > stack_msg{ - ::backtrace_symbols(stack_ptr, static_cast< int >(stack_size)), - [](char** const ptr) { if (ptr) std::free(static_cast< void* >(ptr)); }}; -#if defined(__APPLE__) - const size_t len{ - stack_interpret_apple(stack_ptr, stack_msg.get(), stack_size, output_buf, output_buflen, trim_internal)}; -#else - const size_t len{ - stack_interpret_other(stack_ptr, stack_msg.get(), stack_size, output_buf, output_buflen, trim_internal)}; -#endif -#endif - return len; -} - -[[maybe_unused]] static size_t stack_backtrace(char* const output_buf, const size_t output_buflen, - const bool trim_internal) { - // make this static so no memory allocation needed - static std::mutex s_lock; - static std::array< void*, backtrace_detail::max_backtrace > stack_ptr; - { - std::lock_guard< std::mutex > lock{s_lock}; - const size_t stack_size{stack_backtrace_impl(stack_ptr.data(), stack_ptr.size())}; - return stack_interpret(stack_ptr.data(), stack_size, output_buf, output_buflen, trim_internal); - } -} - -// LCOV_EXCL_STOP diff --git a/src/logging/lib/backtrace.cpp b/src/logging/lib/backtrace.cpp deleted file mode 100644 index f6019526..00000000 --- a/src/logging/lib/backtrace.cpp +++ /dev/null @@ -1,684 +0,0 @@ -/********************************************************************************* - * Modifications Copyright 2017-2019 eBay Inc. - * - * Author/Developer(s): Harihara Kadayam, Bryan Zimmerman - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - *********************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(__linux__) || defined(__APPLE__) -#include -#include -#endif - -#ifdef __linux__ -#include -#endif - -#ifdef __APPLE__ -#include -#include -#endif - -#include "logging.h" - -#include "backtrace.h" - -namespace { - -#ifdef __APPLE__ -[[maybe_unused]] uint64_t static_base_address(void) { - const struct segment_command_64* const command{::getsegbyname(SEG_TEXT /*"__TEXT"*/)}; - const uint64_t addr{command->vmaddr}; - return addr; -} - -[[maybe_unused]] std::string get_exec_path() { - std::array< char, 1024 > path; - uint32_t size{static_cast< uint32_t >(path.size())}; - if (::_NSGetExecutablePath(path.data(), &size) != 0) return std::string{}; - - return std::string{path.data()}; -} - -[[maybe_unused]] std::string get_file_part(const std::string& full_path) { - const size_t pos{full_path.rfind("/")}; - if (pos == std::string::npos) return full_path; - - return full_path.substr(pos + 1, full_path.size() - pos - 1); -} - -[[maybe_unused]] intptr_t image_slide(void) { - const std::string exec_path{get_exec_path()}; - if (exec_path.empty()) return -1; - - const auto image_count{::_dyld_image_count()}; - for (std::remove_const_t< decltype(image_count) > i{0}; i < image_count; ++i) { - if (std::strcmp(::_dyld_get_image_name(i), exec_path.c_str()) == 0) { - return ::_dyld_get_image_vmaddr_slide(i); - } - } - return -1; -} -#endif // __APPLE__ - -template < typename... Args > -[[maybe_unused]] void t_snprintf(char* const msg, size_t& avail_len, size_t& cur_len, size_t& msg_len, Args&&... args) { - avail_len = (avail_len > cur_len) ? (avail_len - cur_len) : 0; - if (avail_len > 0) { - msg_len = std::snprintf(msg + cur_len, avail_len, std::forward< Args >(args)...); - cur_len += (avail_len > msg_len) ? msg_len : avail_len; - } -} - -[[maybe_unused]] uintptr_t convert_hex_to_integer(const char* const input_str) { - uintptr_t actual_addr{0}; - - // Convert hex string -> integer address. - const char* pos{std::strpbrk(input_str, "xX")}; - if (!pos) return actual_addr; - - while (++pos && (*pos != 0x00)) { - const char c{*pos}; - uint8_t val{0}; - if ((c >= '0') && (c <= '9')) { - val = static_cast< uint8_t >(c - '0'); - } else if ((c >= 'A') && (c <= 'F')) { - val = static_cast< uint8_t >((c - 'A') + 10); - } else if ((c >= 'a') && (c <= 'f')) { - val = static_cast< uint8_t >((c - 'a') + 10); - } else { - break; - } - actual_addr <<= 4; - actual_addr += val; - } - return actual_addr; -} - -// trim whitespace of the given null terminated string of length input_length not including null terminator -[[maybe_unused]] size_t trim_whitespace(char* const input, const size_t input_length) { - size_t length{input_length}; - if (length == 0) return length; - - // trim beginning - size_t trim{0}; - while (trim < length) { - if (std::isspace(input[trim]) != 0) - ++trim; - else - break; - } - if (trim > 0) { - length -= trim; - std::memmove(&input[0], &input[trim], length + 1); // include null terminator - } - - // trim end - while (length > 0) { - if (std::isspace(input[length - 1]) != 0) { - input[length - 1] = 0x00; - --length; - } else - break; - } - return length; -} - -[[maybe_unused]] void skip_whitespace(const std::string& base_str, size_t& cursor) { - while ((cursor < base_str.size()) && (base_str[cursor] == ' ')) - ++cursor; -} - -[[maybe_unused]] void skip_glyph(const std::string& base_str, size_t& cursor) { - while ((cursor < base_str.size()) && (base_str[cursor] != ' ')) - ++cursor; -} - -template < typename... Args > -[[maybe_unused]] void log_message(const char* const format, Args&&... args) { - auto& logger{sisl::logging::GetLogger()}; - auto& critical_logger{sisl::logging::GetCriticalLogger()}; - - if (logger) { logger->critical(format, std::forward< Args >(args)...); } - if (critical_logger) { critical_logger->critical(format, std::forward< Args >(args)...); } -} - -#ifdef __linux__ -std::pair< bool, uintptr_t > offset_symbol_address(const char* const file_name, const char* const symbol_str, - const uintptr_t symbol_address) { - bool status{false}; - uintptr_t offset_address{symbol_address}; - Dl_info symbol_info; - - void* addr{nullptr}; - { - const std::unique_ptr< void, std::function< void(void* const) > > obj_file{::dlopen(file_name, RTLD_LAZY), - [](void* const ptr) { - if (ptr) ::dlclose(ptr); - }}; - if (!obj_file) { return {status, offset_address}; } - - addr = ::dlsym(obj_file.get(), symbol_str); - if (!addr) { return {status, offset_address}; } - } - - // extract the symbolic information pointed by address - if (!::dladdr(addr, &symbol_info)) { return {status, offset_address}; } - offset_address += - (reinterpret_cast< uintptr_t >(symbol_info.dli_saddr) - reinterpret_cast< uintptr_t >(symbol_info.dli_fbase)) - - 1; - status = true; - - return {status, offset_address}; -} - -std::pair< const char*, const char* > convert_symbol_line(const char* const file_name, const size_t file_name_length, - const uintptr_t address, const char* const symbol_name) { - static constexpr size_t line_number_length{24}; - static constexpr std::array< char, 10 > s_pipe_unknown{"??\0??:?\0"}; - const char* mangled_name{s_pipe_unknown.data()}; - size_t mangled_name_length{2}; - const char* file_line{s_pipe_unknown.data() + 3}; - size_t file_line_length{4}; - - if (file_name_length == 0) return {mangled_name, file_line}; - - // form the command - static constexpr size_t extra_length{ - 10}; // includes single quotes around process name and " -a 0x" and null terminator - static constexpr std::array< char, 18 > prefix{"addr2line -f -e \'"}; - static std::array< - char, extra_length + prefix.size() + backtrace_detail::file_name_length + backtrace_detail::address_length > - s_command; - size_t command_length{prefix.size() - 1}; - std::memcpy(s_command.data(), prefix.data(), command_length); - std::memcpy(s_command.data() + command_length, file_name, file_name_length); - command_length += file_name_length; - static std::array< char, backtrace_detail::address_length + 1 > s_address; - std::snprintf(s_address.data(), s_address.size(), "%" PRIxPTR, address); - std::snprintf(s_command.data() + command_length, s_command.size() - command_length, "\' -a 0x%s", s_address.data()); - // log_message("SISL Logging - symbol_line with command {}", s_command.data()); - - // execute command and read data from pipe - { - const std::unique_ptr< FILE, std::function< void(FILE* const) > > fp{::popen(s_command.data(), "re"), - [](FILE* const ptr) { - if (ptr) ::pclose(ptr); - }}; - if (fp) { - // wait on pipe - const auto waitOnPipe{[rfd{::fileno(fp.get())}](const uint64_t wait_ms) { - fd_set rfds; - FD_ZERO(&rfds); - FD_SET(rfd, &rfds); - - timespec ts; - ts.tv_sec = static_cast< decltype(ts.tv_sec) >(wait_ms / 1000); - ts.tv_nsec = static_cast< decltype(ts.tv_nsec) >((wait_ms % 1000) * 1000000); - const int result{::pselect(FD_SETSIZE, &rfds, nullptr, nullptr, &ts, nullptr)}; - return (result > 0); - }}; - - // read the pipe - constexpr uint64_t loop_wait_ms{1000}; - constexpr size_t read_tries{static_cast< size_t >(backtrace_detail::pipe_timeout_ms / loop_wait_ms)}; - constexpr size_t newlines_expected{3}; - std::array< const char*, newlines_expected > newline_positions; - size_t total_bytes_read{0}; - size_t total_newlines{0}; - static std::array< - char, backtrace_detail::symbol_name_length + backtrace_detail::file_name_length + line_number_length > - s_pipe_data; - bool address_found{false}; - for (size_t read_try{0}; (read_try < read_tries) && (total_newlines < newlines_expected); ++read_try) { - if (waitOnPipe(loop_wait_ms)) { - size_t bytes{std::fread(s_pipe_data.data() + total_bytes_read, 1, - s_pipe_data.size() - total_bytes_read, fp.get())}; - // count new newlines and null terminate at those positions - for (size_t byte_num{0}; byte_num < bytes; ++byte_num) { - const auto updateNewlines{[&total_newlines, &newline_positions](const size_t offset) { - if (total_newlines < newlines_expected) { - newline_positions[total_newlines] = &s_pipe_data[offset]; - } - ++total_newlines; - }}; - - const size_t offset{byte_num + total_bytes_read}; - if (s_pipe_data[offset] == '\n') { - s_pipe_data[offset] = 0x00; // convert newline to null terminator - if (!address_found) { - // check for address in pipe data - const char* const address_ptr{std::strstr(s_pipe_data.data(), s_address.data())}; - if (address_ptr) { - address_found = true; - updateNewlines(offset); - } else { - // wipe all pipe data up to and including null ptr - if (byte_num < bytes - 1) { - std::memmove(s_pipe_data.data(), s_pipe_data.data() + offset + 1, - bytes + total_bytes_read - offset - 1); - bytes -= byte_num + 1; - } else { - bytes = 0; - } - total_bytes_read = 0; - byte_num = 0; - } - } else { - updateNewlines(offset); - } - } - } - total_bytes_read += bytes; - } - } - s_pipe_data[total_bytes_read] = 0; - - // read the pipe - if (total_newlines > 0) { - if (total_newlines == 3) { - // file and name info - file_line = newline_positions[1] + 1; - file_line_length = static_cast< size_t >(newline_positions[2] - file_line); - file_line_length = trim_whitespace(const_cast< char* >(file_line), file_line_length); - mangled_name = newline_positions[0] + 1; - mangled_name_length = static_cast< size_t >(newline_positions[1] - mangled_name); - mangled_name_length = trim_whitespace(const_cast< char* >(mangled_name), mangled_name_length); - } else if (total_newlines == 2) { - log_message("SISL Logging - Pipe did not return expected number of newlines {}", total_newlines); - mangled_name = newline_positions[0] + 1; - mangled_name_length = static_cast< size_t >(newline_positions[1] - mangled_name); - mangled_name_length = trim_whitespace(const_cast< char* >(mangled_name), mangled_name_length); - } else { - log_message("SISL Logging - Pipe did not return expected number of newlines {}", total_newlines); - } - } else { - // no pipe data just continue - log_message("SISL Logging - No pipe data"); - } - } else { - // no pipe just continue - log_message("SISL Logging - Could not open pipe to resolve symbol_line with command {}", s_command.data()); - } - if (std::strstr(mangled_name, "??")) { - log_message("SISL Logging - Could not resolve symbol_line with command {}", s_command.data()); - } - } - - // demangle the name - static std::array< char, backtrace_detail::symbol_name_length > demangled_name; - { - [[maybe_unused]] int status{-3}; // one of the arguments is invalid - const std::unique_ptr< const char, std::function< void(const char* const) > > cxa_demangled_name{ - std::strstr(mangled_name, "??") ? nullptr : abi::__cxa_demangle(mangled_name, 0, 0, &status), - [](const char* const ptr) { - if (ptr) std::free(static_cast< void* >(const_cast< char* >(ptr))); - }}; - if (!cxa_demangled_name) { - if (status != -2) { // check that not a mangled name - log_message("SISL Logging - Could not demangle name {} error {}", mangled_name, status); - } - if (!symbol_name || (symbol_name[0] == '+') || (symbol_name[0] == 0x00)) { - // no symbol name so use mangled name - std::memcpy(demangled_name.data(), mangled_name, mangled_name_length); - demangled_name[mangled_name_length] = 0x00; - } else { - // use the symbol name - std::snprintf(demangled_name.data(), demangled_name.size(), "%s", symbol_name); - } - } else { - // use the demangled name - std::snprintf(demangled_name.data(), demangled_name.size(), "%s", cxa_demangled_name.get()); - } - } - - // resolve file name absolute path - static std::array< char, backtrace_detail::file_name_length + line_number_length > s_absolute_file_path; - static std::array< char, backtrace_detail::file_name_length > s_relative_file_path; - const char* const colon_ptr{std::strrchr(file_line, ':')}; - const size_t relative_file_name_length{colon_ptr ? static_cast< size_t >(colon_ptr - file_line) : file_line_length}; - if (std::strstr(file_line, "??") || (relative_file_name_length == 0)) { - // no resolved file name, use process/lib name - std::memcpy(s_relative_file_path.data(), file_name, file_name_length); - s_relative_file_path[file_name_length] = 0x00; - } else { - // use previoulsy received possibly relative path - std::memcpy(s_relative_file_path.data(), file_line, relative_file_name_length); - s_relative_file_path[relative_file_name_length] = 0x00; - } - if (const char* const path{::realpath(s_relative_file_path.data(), s_absolute_file_path.data())}) { - // absolute path resolved - } else { - // use the relative file name path - std::strcpy(s_absolute_file_path.data(), s_relative_file_path.data()); - } - // append line number - if (colon_ptr) { - std::strcat(s_absolute_file_path.data(), colon_ptr); - } else { - std::strcat(s_absolute_file_path.data(), ":?"); - } - - return {demangled_name.data(), s_absolute_file_path.data()}; -} - -#endif // __linux__ - -} // anonymous namespace - -#ifdef __linux__ - -const char* linux_process_name() { - static std::array< char, backtrace_detail::file_name_length > s_process_name; - const auto length{::readlink("/proc/self/exe", s_process_name.data(), s_process_name.size())}; - if (length == -1) { - s_process_name[0] = 0; - } else if (static_cast< size_t >(length) == s_process_name.size()) { - // truncation occurred so null terminate - s_process_name[s_process_name.size() - 1] = 0; - } else { - // success so null terminate - s_process_name[static_cast< size_t >(length)] = 0; - } - return s_process_name.data(); -} - -size_t stack_interpret_linux_file(const void* const* const stack_ptr, FILE* const stack_file, const size_t stack_size, - char* const output_buf, const size_t output_buflen, const bool trim_internal) { - std::rewind(stack_file); - char c{0x00}; - - /* - while (!feof(stack_file)) std::putc(fgetc(stack_file), stdout); - std::rewind(stack_file); - */ - - // get the current process name - const char* const absolute_process_name{linux_process_name()}; - const char* const slash_pos{std::strrchr(absolute_process_name, '/')}; - const char* const process_name{slash_pos ? slash_pos + 1 : absolute_process_name}; - const size_t process_name_length{std::strlen(process_name)}; - - static std::array< size_t, backtrace_detail::max_backtrace > s_output_line_start; - size_t cur_len{0}; - size_t chars_read{0}; - const auto extractName{[&stack_file, &c, &chars_read](auto& dest, const auto& term_chars) { - size_t len{0}; - const auto nullTerminate{[&len, &dest]() { - if (len < dest.size()) { - dest[len] = 0x00; - } else { - dest[dest.size() - 1] = 0x00; - } - return std::min(len, dest.size() - 1); - }}; - while (!std::feof(stack_file)) { - c = static_cast< char >(std::fgetc(stack_file)); - if (!std::feof(stack_file)) { - ++chars_read; - if (std::find(std::cbegin(term_chars), std::cend(term_chars), c) != std::cend(term_chars)) { - return nullTerminate(); - } else if (len < dest.size()) { - dest[len] = c; - } - ++len; - } - } - return nullTerminate(); - }}; - - // read till end of line - const auto readTillEOL{[&stack_file, &c, &chars_read]() { - while (!std::feof(stack_file)) { - c = static_cast< char >(std::fgetc(stack_file)); - if (!std::feof(stack_file)) { - ++chars_read; - if (c == '\n') return; - } - } - return; - }}; - - size_t trim_line{0}; - size_t line_num{0}; - size_t msg_len{0}; - size_t avail_len{output_buflen}; - // NOTE: starting from 1, skipping this line. - readTillEOL(); - for (size_t i{1}; (i < stack_size) && !std::feof(stack_file); ++i) { - // `stack_msg[x]` format: - // /foo/bar/executable() [0xabcdef] - // /foo/bar/executable()(+0xf0) [0x123456] - // /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x123456] - - // NOTE: with ASLR - // /foo/bar/executable(+0x5996) [0x555555559996] - - static std::array< char, backtrace_detail::file_name_length > s_file_name; - const size_t file_name_length{ - trim_whitespace(s_file_name.data(), extractName(s_file_name, std::array< char, 2 >{'(', '\n'}))}; - - if (file_name_length == 0) { - if (c != '\n') readTillEOL(); - continue; - } - - uintptr_t actual_addr{reinterpret_cast< uintptr_t >(stack_ptr[i])}; - static std::array< char, backtrace_detail::symbol_name_length > s_symbol; - s_symbol[0] = 0x00; - if (c == '(') { - // Extract the symbol if present - const size_t symbol_len{ - trim_whitespace(s_symbol.data(), extractName(s_symbol, std::array< char, 2 >{')', '\n'}))}; - - // Extract the offset - if (symbol_len > 0) { - char* const plus{std::strchr(s_symbol.data(), '+')}; - const uintptr_t symbol_address{plus ? convert_hex_to_integer(plus + 1) : 0}; - - if (plus == s_symbol.data()) { - // ASLR is enabled, get the offset from here. - actual_addr = symbol_address; - } else { - if (plus) { - // truncate symbol at + so just function name - *plus = 0x00; - } - const bool main_program{ - file_name_length < process_name_length - ? false - : std::strcmp(process_name, s_file_name.data() + file_name_length - process_name_length) == - 0}; - const auto [offset_result, offset_addr]{offset_symbol_address( - main_program ? nullptr : s_file_name.data(), s_symbol.data(), symbol_address)}; - if (offset_result) { - actual_addr = offset_addr; - } else { - log_message( - "SISL Logging - Could not resolve offset_symbol_address for symbol {} with address {}", - s_symbol.data(), symbol_address); - } - } - } - } - - const auto [demangled_name, - file_line]{convert_symbol_line(s_file_name.data(), file_name_length, actual_addr, s_symbol.data())}; - if (!demangled_name || !file_line) { - if (c != '\n') readTillEOL(); - continue; - } - - if (trim_internal) { - if (std::strstr(demangled_name, "sisl::logging::bt_dumper") || - std::strstr(demangled_name, "sisl::logging::crash_handler")) { - trim_line = line_num; - } - } - s_output_line_start[line_num] = cur_len; - t_snprintf(output_buf, avail_len, cur_len, msg_len, "#%-3zu 0x%016" PRIxPTR " in %s at %s\n", line_num, - actual_addr, demangled_name, file_line); - ++line_num; - - if (c != '\n') readTillEOL(); - } - - if (trim_line > 0) { - // trim characters and include null character at end - const size_t offset{s_output_line_start[trim_line]}; - cur_len -= offset; - std::memmove(output_buf, output_buf + offset, cur_len + 1); // move terminating null - - // renumber lines - for (size_t current_line{0}; current_line < line_num - trim_line; ++current_line) { - std::array< char, 5 > line_str; - const int length{std::snprintf(line_str.data(), line_str.size(), "#%-3zu", current_line)}; - std::memcpy(output_buf + s_output_line_start[trim_line + current_line] - offset, line_str.data(), length); - } - } - - return cur_len; -} -#endif // __linux__ - -#ifdef __APPLE__ -size_t stack_interpret_apple([[maybe_unused]] const void* const* const stack_ptr, const char* const* const stack_msg, - const size_t stack_size, char* const output_buf, const size_t output_buflen, , - [[maybe_unused]] const bool trim_internal) { - size_t cur_len{0}; - - [[maybe_unused]] size_t frame_num{0}; - - const std::string exec_full_path{get_exec_path()}; - const std::string exec_file{get_file_part(exec_full_path)}; - const uint64_t load_base{static_cast< uint64_t >(image_slide()) + static_base_address()}; - - // NOTE: starting from 1, skipping this frame. - for (size_t i{1}; i < stack_size; ++i) { - // `stack_msg[x]` format: - // 8 foobar 0x000000010fd490da main + 1322 - if (!stack_msg[i] || (stack_msg[i][0] == 0x0)) continue; - - const std::string base_str{stack_msg[i]}; - - size_t s_pos{0}; - size_t len{0}; - size_t cursor{0}; - - // Skip frame number part. - skip_glyph(base_str, cursor); - - // Skip whitespace. - skip_whitespace(base_str, cursor); - s_pos = cursor; - // Filename part. - skip_glyph(base_str, cursor); - len = cursor - s_pos; - const std::string filename{base_str.substr(s_pos, len)}; - - // Skip whitespace. - skip_whitespace(base_str, cursor); - s_pos = cursor; - // Address part. - skip_glyph(base_str, cursor); - len = cursor - s_pos; - const std::string address{base_str.substr(s_pos, len)}; - if (!address.empty() && address[0] == '?') continue; - - // Skip whitespace. - skip_whitespace(base_str, cursor); - s_pos = cursor; - // Mangled function name part. - skip_glyph(base_str, cursor); - len = cursor - s_pos; - const std::string func_mangled{base_str.substr(s_pos, len)}; - - size_t msg_len{0}; - size_t avail_len = output_buflen; - - t_snprintf(output_buf, avail_len, cur_len, msg_len, "#%-3zu %s in ", frame_num++, address.c_str()); - - if (filename != exec_file) { - // Dynamic library. - int status; - const std::unique_ptr< const char, std::function< void(const char* const) > > cc{ - abi::__cxa_demangle(func_mangled.c_str(), 0, 0, &status), [](const char* const ptr) { - if (ptr) std::free(static_cast< void* >(const_cast< char* >(ptr))); - }}; - if (cc) { - t_snprintf(output_buf, avail_len, cur_len, msg_len, "%s at %s\n", cc.get(), filename.c_str()); - } else { - t_snprintf(output_buf, avail_len, cur_len, msg_len, "%s() at %s\n", func_mangled.c_str(), - filename.c_str()); - } - } else { - // atos return format: - // bbb(char) (in crash_example) (crash_example.cc:37) - std::ostringstream ss; - ss << "atos -l 0x"; - ss << std::hex << load_base; - ss << " -o " << exec_full_path; - ss << " " << address; - const std::unique_ptr< FILE, std::function< void(FILE* const) > > fp{::popen(ss.str().c_str() "r"), - [](FILE* const ptr) { - if (ptr) ::pclose(ptr); - }}; - if (!fp) continue; - - std::array< char, 4096 > atos_cstr; - std::fgets(atos_cstr.data(), atos_cstr.size() - 1, fp); - - const std::string atos_str{atos_cstr.data()}; - size_t d_pos{atos_str.find(" (in ")}; - if (d_pos == std::string::npos) continue; - const std::string function_part{atos_str.substr(0, d_pos)}; - - d_pos = atos_str.find(") (", d_pos); - if (d_pos == std::string::npos) continue; - std::string source_part{atos_str.substr(d_pos + 3)}; - source_part = source_part.substr(0, source_part.size() - 2); - - t_snprintf(output_buf, avail_len, cur_len, msg_len, "%s at %s\n", function_part.c_str(), - source_part.c_str()); - } - } - - return cur_len; -} -#endif // __APPLE__ - -size_t stack_interpret_other([[maybe_unused]] const void* const* const stack_ptr, const char* const* const stack_msg, - const size_t stack_size, char* const output_buf, const size_t output_buflen, - [[maybe_unused]] const bool trim_internal) { - size_t cur_len{0}; - [[maybe_unused]] size_t frame_num{0}; - - // NOTE: starting from 1, skipping this frame. - for (size_t i{1}; i < stack_size; ++i) { - // On non-Linux platform, just use the raw symbols. - size_t msg_len{0}; - size_t avail_len{output_buflen}; - t_snprintf(output_buf, avail_len, cur_len, msg_len, "%s\n", stack_msg[i]); - } - return cur_len; -} diff --git a/src/logging/lib/logging.cpp b/src/logging/logging.cpp similarity index 74% rename from src/logging/lib/logging.cpp rename to src/logging/logging.cpp index d66edf48..f2bd808d 100644 --- a/src/logging/lib/logging.cpp +++ b/src/logging/logging.cpp @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ * *********************************************************************************/ -#include "logging.h" +#include #include #include @@ -26,24 +26,23 @@ #include #if defined(__linux__) || defined(__APPLE__) +#if defined(__APPLE__) +#undef _POSIX_C_SOURCE +#endif #include #include #endif #if defined(__linux__) #include -#elif defined(__APPLE__) -#include #endif -#include "options/options.h" +#include #include #include #include #include -#include "backtrace.h" - // clang-format off SISL_OPTION_GROUP(logging, (enab_mods, "", "log_mods", "Module loggers to enable", ::cxxopts::value(), "mod[:level][,mod2[:level2],...]"), \ (async_size, "", "log_queue", "Size of async log queue", ::cxxopts::value()->default_value("4096"), "(power of 2)"), \ @@ -58,10 +57,7 @@ SISL_OPTION_GROUP(logging, (enab_mods, "", "log_mods", "Module loggers to enabl (version, "V", "version", "Print the version and exist", ::cxxopts::value(), "")) // clang-format on -// logger required define if not inited -extern "C" { -spdlog::level::level_enum module_level_base{spdlog::level::level_enum::info}; -} +SISL_LOGGING_DEF(base) namespace sisl { namespace logging { @@ -77,31 +73,34 @@ static constexpr size_t MAX_MODULES{100}; static std::array< const char*, MAX_MODULES > glob_enabled_mods{"base"}; static size_t glob_num_mods{1}; -/****************************** LoggerThreadContext ******************************/ -std::mutex LoggerThreadContext::s_logger_thread_mutex; -std::unordered_set< LoggerThreadContext* > LoggerThreadContext::s_logger_thread_set; +/****************************** LoggerThreadRegistry ******************************/ +std::shared_ptr< LoggerThreadRegistry > LoggerThreadRegistry::instance() { + static std::shared_ptr< LoggerThreadRegistry > inst{std::make_shared< LoggerThreadRegistry >()}; + return inst; +} -LoggerThreadContext::LoggerThreadContext() { - m_thread_id = pthread_self(); - LoggerThreadContext::add_logger_thread(this); +void LoggerThreadRegistry::add_logger_thread(LoggerThreadContext* ctx) { + std::unique_lock l{m_logger_thread_mutex}; + m_logger_thread_set.insert(ctx); } -LoggerThreadContext::~LoggerThreadContext() { LoggerThreadContext::remove_logger_thread(this); } +void LoggerThreadRegistry::remove_logger_thread(LoggerThreadContext* ctx) { + std::unique_lock l{m_logger_thread_mutex}; + m_logger_thread_set.erase(ctx); +} +/****************************** LoggerThreadContext ******************************/ LoggerThreadContext& LoggerThreadContext::instance() { static thread_local LoggerThreadContext inst{}; return inst; } -void LoggerThreadContext::add_logger_thread(LoggerThreadContext* const ctx) { - std::unique_lock l{s_logger_thread_mutex}; - s_logger_thread_set.insert(ctx); +LoggerThreadContext::LoggerThreadContext() : + m_thread_id{pthread_self()}, m_logger_thread_registry{LoggerThreadRegistry::instance()} { + m_logger_thread_registry->add_logger_thread(this); } -void LoggerThreadContext::remove_logger_thread(LoggerThreadContext* const ctx) { - std::unique_lock l{s_logger_thread_mutex}; - s_logger_thread_set.erase(ctx); -} +LoggerThreadContext::~LoggerThreadContext() { m_logger_thread_registry->remove_logger_thread(this); } /******************************** InitModules *********************************/ void InitModules::init_modules(std::initializer_list< const char* > mods_list) { @@ -133,28 +132,42 @@ std::shared_ptr< spdlog::logger >& GetCriticalLogger() { return logger_thread_ctx.m_critical_logger; } -static std::filesystem::path get_base_dir() { - const auto cwd{std::filesystem::current_path()}; - auto p{cwd / "logs"}; +static std::filesystem::path g_base_dir; + +std::filesystem::path get_base_dir() { + namespace fs = std::filesystem; + const auto cwd = fs::current_path(); + const auto log_dir{cwd / "logs"}; + // Construct a unique directory path based on the current time auto const current_time{std::chrono::system_clock::now()}; auto const current_t{std::chrono::system_clock::to_time_t(current_time)}; auto const current_tm{std::localtime(¤t_t)}; std::array< char, PATH_MAX > c_time; if (std::strftime(c_time.data(), c_time.size(), "%F_%R", current_tm)) { - p /= c_time.data(); - std::filesystem::create_directories(p); + const fs::path cur_log_dir = log_dir / c_time.data(); + fs::create_directories(cur_log_dir); + + const fs::path sym_path = log_dir / "latest"; + try { + if (fs::is_symlink(sym_path)) { fs::remove(sym_path); } + fs::create_directory_symlink(cur_log_dir, sym_path); + } catch (std::exception& e) { + LOGINFO("Unable to create latest symlink 'latest' to logdir, ignoring symlink creation\n"); + } + return cur_log_dir; + } else { + return log_dir; } - return p; } + static std::filesystem::path log_path(std::string const& name) { std::filesystem::path p; if (0 < SISL_OPTIONS.count("logfile")) { p = std::filesystem::path{SISL_OPTIONS["logfile"].as< std::string >()}; } else { - static std::filesystem::path base_dir{get_base_dir()}; - p = base_dir / std::filesystem::path{name}.filename(); + p = get_base_dir() / std::filesystem::path{name}.filename(); } return p; } @@ -215,15 +228,19 @@ void set_global_logger(N const& name, S const& sinks, S const& crit_sinks) { spdlog::register_logger(glob_critical_logger); } -static void set_module_log_level(const std::string& module_name, const spdlog::level::level_enum level) { - const auto sym{std::string{"module_level_"} + module_name}; - auto* const mod_level{static_cast< spdlog::level::level_enum* >(::dlsym(RTLD_DEFAULT, sym.c_str()))}; +static spdlog::level::level_enum* to_mod_log_level_ptr(const std::string& module_name) { + const auto sym = std::string{"module_level_"} + module_name; + auto* mod_level = static_cast< spdlog::level::level_enum* >(::dlsym(RTLD_DEFAULT, sym.c_str())); if (mod_level == nullptr) { - LOGWARN("Unable to locate the module {} in registered modules", module_name); - return; + std::cout << fmt::format("Unable to locate the module {} in registered modules, error: {}\n", module_name, + dlerror()); } + return mod_level; +} - *mod_level = level; +static void set_module_log_level(const std::string& module_name, const spdlog::level::level_enum level) { + auto* mod_level = to_mod_log_level_ptr(module_name); + if (mod_level != nullptr) { *mod_level = level; } } static std::string setup_modules() { @@ -243,37 +260,38 @@ static std::string setup_modules() { fmt::vformat_to(std::back_inserter(out_str), fmt::string_view{"{}={}, "}, fmt::make_format_args(mod_name, lvl_str)); } - } else { - if (SISL_OPTIONS.count("log_mods")) { - std::regex re{"[\\s,]+"}; - const auto s{SISL_OPTIONS["log_mods"].as< std::string >()}; - std::sregex_token_iterator it{std::cbegin(s), std::cend(s), re, -1}; - std::sregex_token_iterator reg_end; - for (; it != reg_end; ++it) { - auto mod_stream{std::istringstream(it->str())}; - std::string module_name, module_level; - std::getline(mod_stream, module_name, ':'); - const auto sym{std::string{"module_level_"} + module_name}; - if (auto* const mod_level{ - static_cast< spdlog::level::level_enum* >(::dlsym(RTLD_DEFAULT, sym.c_str()))}; - nullptr != mod_level) { - if (std::getline(mod_stream, module_level, ':')) { - *mod_level = (1 == module_level.size()) - ? static_cast< spdlog::level::level_enum >(std::strtol(module_level.data(), nullptr, 0)) - : spdlog::level::from_str(module_level.data()); - } - } else { - LOGWARN("Could not load module logger: {}\n{}", module_name, dlerror()); + } else + set_module_log_level("base", spdlog::level::level_enum::info); + + if (SISL_OPTIONS.count("log_mods")) { + std::regex re{"[\\s,]+"}; + const auto s{SISL_OPTIONS["log_mods"].as< std::string >()}; + std::sregex_token_iterator it{std::cbegin(s), std::cend(s), re, -1}; + std::sregex_token_iterator reg_end; + for (; it != reg_end; ++it) { + auto mod_stream{std::istringstream(it->str())}; + std::string module_name, module_level; + std::getline(mod_stream, module_name, ':'); + const auto sym{std::string{"module_level_"} + module_name}; + if (auto* const mod_level{static_cast< spdlog::level::level_enum* >(::dlsym(RTLD_DEFAULT, sym.c_str()))}; + nullptr != mod_level) { + if (std::getline(mod_stream, module_level, ':')) { + *mod_level = (1 == module_level.size()) + ? static_cast< spdlog::level::level_enum >(std::strtol(module_level.data(), nullptr, 0)) + : spdlog::level::from_str(module_level.data()); } + } else { + std::cout << fmt::format("Unable to locate the module {} in registered modules, error: {}\n", + module_name, dlerror()); } } + } - for (size_t mod_num{0}; mod_num < glob_num_mods; ++mod_num) { - const std::string& mod_name{glob_enabled_mods[mod_num]}; - fmt::vformat_to( - std::back_inserter(out_str), fmt::string_view{"{}={}, "}, - fmt::make_format_args(mod_name, spdlog::level::to_string_view(GetModuleLogLevel(mod_name)).data())); - } + for (size_t mod_num{0}; mod_num < glob_num_mods; ++mod_num) { + const std::string& mod_name{glob_enabled_mods[mod_num]}; + fmt::vformat_to( + std::back_inserter(out_str), fmt::string_view{"{}={}, "}, + fmt::make_format_args(mod_name, spdlog::level::to_string_view(GetModuleLogLevel(mod_name)).data())); } return out_str; @@ -347,14 +365,8 @@ void SetModuleLogLevel(const std::string& module_name, const spdlog::level::leve } spdlog::level::level_enum GetModuleLogLevel(const std::string& module_name) { - const auto sym{std::string{"module_level_"} + module_name}; - auto* const mod_level{static_cast< spdlog::level::level_enum* >(::dlsym(RTLD_DEFAULT, sym.c_str()))}; - if (mod_level == nullptr) { - LOGWARN("Unable to locate the module {} in registered modules", module_name); - return spdlog::level::level_enum::off; - } - - return *mod_level; + auto* mod_level = to_mod_log_level_ptr(module_name); + return mod_level ? *mod_level : spdlog::level::level_enum::off; } nlohmann::json GetAllModuleLogLevel() { diff --git a/src/logging/lib/stacktrace.cpp b/src/logging/stacktrace.cpp similarity index 71% rename from src/logging/lib/stacktrace.cpp rename to src/logging/stacktrace.cpp index f96711b4..ac65d5d1 100644 --- a/src/logging/lib/stacktrace.cpp +++ b/src/logging/stacktrace.cpp @@ -32,23 +32,17 @@ #include #endif -#include "backtrace.h" -#include "logging.h" - -namespace { -constexpr uint64_t backtrace_timeout_ms{4 * backtrace_detail::pipe_timeout_ms}; -} +#include +#if defined(__linux__) +#include +#endif namespace sisl { namespace logging { static bool g_custom_signal_handler_installed{false}; static size_t g_custom_signal_handlers{0}; static bool g_crash_handle_all_threads{true}; -static std::mutex g_mtx_stack_dump_outstanding; -static size_t g_stack_dump_outstanding{0}; -static std::condition_variable g_stack_dump_cv; static std::mutex g_hdlr_mutex; -static std::array< char, max_stacktrace_size() > g_stacktrace_buff; typedef struct SignalHandlerData { SignalHandlerData(std::string name, const sig_handler_t handler) : name{std::move(name)}, handler{handler} {} @@ -64,8 +58,10 @@ static bool exit_in_progress() { pthread_t current_id{tracing_thread_id.load()}; pthread_t new_id{pthread_self()}; +#ifndef __APPLE__ if (logger) { logger->critical("Thread num: {} entered exit handler\n", new_id); } if (critical_logger) { critical_logger->critical("Thread num: {} entered exit handler\n", new_id); } +#endif if (current_id == new_id) { // we are already marked in exit handler @@ -125,7 +121,23 @@ static const char* exit_reason_name(const SignalType fatal_id) { } } +#if defined(__linux__) +static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, [[maybe_unused]] void*, + bool succeeded) { + std::cerr << std::endl << "Minidump path: " << descriptor.path() << std::endl; + return succeeded; +} +#endif + +static void bt_dumper([[maybe_unused]] const SignalType signal_number) { +#if defined(__linux__) + google_breakpad::ExceptionHandler::WriteMinidump(get_base_dir().string(), dumpCallback, nullptr); +#endif +} + static void crash_handler(const SignalType signal_number) { + LOGCRITICAL("\n * ****Received fatal SIGNAL : {}({})\tPID : {}", exit_reason_name(signal_number), signal_number, + ::getpid()); const auto flush_logs{[]() { // flush all logs spdlog::apply_all([&](std::shared_ptr< spdlog::logger > l) { if (l) l->flush(); @@ -143,17 +155,10 @@ static void crash_handler(const SignalType signal_number) { } else { flush_logs(); } - - // remove all default logging info except for message - GetLogger()->set_pattern("%v"); - log_stack_trace(g_crash_handle_all_threads); - LOGCRITICAL("\n * ****Received fatal SIGNAL : {}({})\tPID : {}", exit_reason_name(signal_number), signal_number, - ::getpid()); - - // flush again and shutdown - flush_logs(); spdlog::shutdown(); + bt_dumper(signal_number); + exit_with_default_sighandler(signal_number); } @@ -168,94 +173,6 @@ static void sigint_handler(const SignalType signal_number) { exit_with_default_sighandler(signal_number); } -static void bt_dumper([[maybe_unused]] const SignalType signal_number) { - g_stacktrace_buff.fill(0); - stack_backtrace(g_stacktrace_buff.data(), g_stacktrace_buff.size(), true); - bool notify{false}; - { - std::unique_lock lock{g_mtx_stack_dump_outstanding}; - if (g_stack_dump_outstanding > 0) { - --g_stack_dump_outstanding; - notify = true; - } - } - if (notify) g_stack_dump_cv.notify_all(); -} - -static void log_stack_trace_all_threads() { - std::unique_lock logger_lock{LoggerThreadContext::s_logger_thread_mutex}; - auto& logger{GetLogger()}; - auto& critical_logger{GetCriticalLogger()}; - size_t thread_count{1}; - - const auto dump_thread{[&logger, &critical_logger, &thread_count](const bool signal_thread, const auto thread_id) { - if (signal_thread) { - const auto log_failure{[&logger, &critical_logger, &thread_count, &thread_id](const char* const msg) { - if (logger) { - logger->critical("Thread ID: {}, Thread num: {} - {}\n", thread_id, thread_count, msg); - logger->flush(); - } - if (critical_logger) { - critical_logger->critical("Thread ID: {}, Thread num: {} - {}\n", thread_id, thread_count, msg); - critical_logger->flush(); - } - }}; - - { - std::unique_lock outstanding_lock{g_mtx_stack_dump_outstanding}; - assert(g_stack_dump_outstanding == 0); - g_stack_dump_outstanding = 1; - } - if (!send_thread_signal(thread_id, SIGUSR3)) { - { - std::unique_lock outstanding_lock{g_mtx_stack_dump_outstanding}; - g_stack_dump_outstanding = 0; - } - log_failure("Invalid/terminated thread"); - return; - } - - { - std::unique_lock outstanding_lock{g_mtx_stack_dump_outstanding}; - const auto result{g_stack_dump_cv.wait_for(outstanding_lock, - std::chrono::milliseconds{backtrace_timeout_ms}, - [] { return (g_stack_dump_outstanding == 0); })}; - if (!result) { - g_stack_dump_outstanding = 0; - outstanding_lock.unlock(); - log_failure("Timeout waiting for stacktrace"); - return; - } - } - } else { - // dump the thread without recursive signal - g_stacktrace_buff.fill(0); - stack_backtrace(g_stacktrace_buff.data(), g_stacktrace_buff.size(), true); - } - - if (logger) { - logger->critical("Thread ID: {}, Thread num: {}\n{}", thread_id, thread_count, g_stacktrace_buff.data()); - logger->flush(); - } - if (critical_logger) { - critical_logger->critical("Thread ID: {}, Thread num: {}\n{}", thread_id, thread_count, - g_stacktrace_buff.data()); - critical_logger->flush(); - } - }}; - - // First dump this thread context - dump_thread(false, logger_thread_ctx.m_thread_id); - ++thread_count; - - // dump other threads - for (auto* const ctx : LoggerThreadContext::s_logger_thread_set) { - if (ctx == &logger_thread_ctx) { continue; } - dump_thread(true, ctx->m_thread_id); - ++thread_count; - } -} - /************************************************* Exported APIs **********************************/ static std::map< SignalType, signame_handler_data_t > g_sighandler_map{ {SIGABRT, {"SIGABRT", &crash_handler}}, @@ -413,18 +330,6 @@ void log_custom_signal_handlers() { LOGINFO("Custom Signal handlers: {}", m); } -void log_stack_trace(const bool all_threads) { - if (is_crash_handler_installed() && all_threads) { - log_stack_trace_all_threads(); - } else { - // make this static so that no memory allocation is necessary - static std::array< char, max_stacktrace_size() > buff; - buff.fill(0); - [[maybe_unused]] const size_t s{stack_backtrace(buff.data(), buff.size(), true)}; - LOGCRITICAL("\n\n{}", buff.data()); - } -} - bool send_thread_signal(const pthread_t thr, const SignalType sig_num) { return (::pthread_kill(thr, sig_num) == 0); } bool install_crash_handler(const bool all_threads) { return install_signal_handler(all_threads); } diff --git a/src/logging/test/example.cpp b/src/logging/test/example.cpp index 17abe762..adc4a675 100644 --- a/src/logging/test/example.cpp +++ b/src/logging/test/example.cpp @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,12 @@ #include #include -#include "options/options.h" +#include -#include "logging.h" +#include +SISL_LOGGING_DECL(my_module) +SISL_LOGGING_DEF(my_module) SISL_LOGGING_INIT(my_module) void func() { diff --git a/src/metrics/CMakeLists.txt b/src/metrics/CMakeLists.txt index 0720f949..fd5e7c09 100644 --- a/src/metrics/CMakeLists.txt +++ b/src/metrics/CMakeLists.txt @@ -1,36 +1,41 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-attributes") # needed for C++ 20 folly compilation -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) -set(METRICS_SOURCE_FILES +add_library(sisl_metrics OBJECT) +target_sources(sisl_metrics PRIVATE metrics.cpp metrics_atomic.cpp metrics_group_impl.cpp metrics_rcu.cpp metrics_tlocal.cpp - ) -add_library(sisl_metrics OBJECT ${METRICS_SOURCE_FILES}) + ) target_link_libraries(sisl_metrics ${COMMON_DEPS} - Folly::Folly + folly::folly ) -set(FARM_TEST_SOURCES tests/farm_test.cpp) -add_executable(metrics_farm_test ${FARM_TEST_SOURCES}) -target_link_libraries(metrics_farm_test sisl ${COMMON_DEPS} GTest::gtest) -add_test(NAME MetricsFarmTest COMMAND metrics_farm_test) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(metrics_farm_test) + target_sources(metrics_farm_test PRIVATE + tests/farm_test.cpp + ) + target_link_libraries(metrics_farm_test sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME MetricsFarm COMMAND metrics_farm_test) -set(WRAPPER_TEST_SOURCES tests/wrapper_test.cpp) -add_executable(metrics_wrapper_test ${WRAPPER_TEST_SOURCES}) -target_link_libraries(metrics_wrapper_test sisl ${COMMON_DEPS} GTest::gtest) -add_test(NAME MetricsWrapperTest COMMAND metrics_wrapper_test) + add_executable(metrics_wrapper_test) + target_sources(metrics_wrapper_test PRIVATE + tests/wrapper_test.cpp + ) + target_link_libraries(metrics_wrapper_test sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME MetricsWrapper COMMAND metrics_wrapper_test) -set(METRICS_BENCHMARK_SOURCES tests/metrics_benchmark.cpp) -add_executable(metrics_benchmark ${METRICS_BENCHMARK_SOURCES}) -target_link_libraries(metrics_benchmark sisl ${COMMON_DEPS} benchmark::benchmark) -add_test(NAME MetricsBenchmarkTest COMMAND metrics_benchmark) + add_executable(metrics_benchmark) + target_sources(metrics_benchmark PRIVATE + tests/metrics_benchmark.cpp + ) + target_link_libraries(metrics_benchmark sisl ${COMMON_DEPS} benchmark::benchmark) + add_test(NAME MetricsBenchmark COMMAND metrics_benchmark) + endif() +endif() diff --git a/src/metrics/metrics.cpp b/src/metrics/metrics.cpp index 34910d73..553e122c 100644 --- a/src/metrics/metrics.cpp +++ b/src/metrics/metrics.cpp @@ -14,9 +14,9 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include "logging/logging.h" +#include -#include "metrics.hpp" +#include "sisl/metrics/metrics.hpp" THREAD_BUFFER_INIT diff --git a/src/metrics/metrics_atomic.cpp b/src/metrics/metrics_atomic.cpp index 9cecebee..ad5c5b01 100644 --- a/src/metrics/metrics_atomic.cpp +++ b/src/metrics/metrics_atomic.cpp @@ -18,9 +18,9 @@ #include #include -#include "logging/logging.h" +#include -#include "metrics_atomic.hpp" +#include "sisl/metrics/metrics_atomic.hpp" namespace sisl { diff --git a/src/metrics/metrics_group_impl.cpp b/src/metrics/metrics_group_impl.cpp index e21d0af2..0b709bf4 100644 --- a/src/metrics/metrics_group_impl.cpp +++ b/src/metrics/metrics_group_impl.cpp @@ -26,11 +26,11 @@ #endif #include -#include "logging/logging.h" +#include -#include "metrics_group_impl.hpp" -#include "metrics.hpp" -#include "metrics_tlocal.hpp" +#include "sisl/metrics/metrics_group_impl.hpp" +#include "sisl/metrics/metrics.hpp" +#include "sisl/metrics/metrics_tlocal.hpp" namespace sisl { @@ -240,7 +240,7 @@ nlohmann::json MetricsGroupImpl::get_result_in_json(bool need_latest) { HistogramDynamicInfo& h = hist_dynamic_info(idx); if (h.is_histogram_reporter()) { hist_entries[hist_static_info(idx).desc()] = - fmt::format("{:#} / {:#} / {:#} / {:#}", h.average(result), + fmt::format("{:.1f} / {:.1f} / {:.1f} / {:.1f}", h.average(result), h.percentile(result, hist_static_info(idx).get_boundaries(), 50), h.percentile(result, hist_static_info(idx).get_boundaries(), 95), h.percentile(result, hist_static_info(idx).get_boundaries(), 99)); diff --git a/src/metrics/metrics_rcu.cpp b/src/metrics/metrics_rcu.cpp index f70de42f..bd69c34f 100644 --- a/src/metrics/metrics_rcu.cpp +++ b/src/metrics/metrics_rcu.cpp @@ -14,8 +14,8 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include "metrics_rcu.hpp" -#include "logging/logging.h" +#include "sisl/metrics/metrics_rcu.hpp" +#include namespace sisl { diff --git a/src/metrics/metrics_tlocal.cpp b/src/metrics/metrics_tlocal.cpp index f0d40671..795d2be3 100644 --- a/src/metrics/metrics_tlocal.cpp +++ b/src/metrics/metrics_tlocal.cpp @@ -18,9 +18,9 @@ #include #include -#include "logging/logging.h" +#include -#include "metrics_tlocal.hpp" +#include "sisl/metrics/metrics_tlocal.hpp" namespace sisl { diff --git a/src/metrics/tests/farm_test.cpp b/src/metrics/tests/farm_test.cpp index a84f581a..451bda93 100644 --- a/src/metrics/tests/farm_test.cpp +++ b/src/metrics/tests/farm_test.cpp @@ -23,9 +23,9 @@ #include #include -#include "logging/logging.h" +#include -#include "metrics.hpp" +#include "sisl/metrics/metrics.hpp" constexpr size_t ITERATIONS{3}; diff --git a/src/metrics/tests/functionality_test.cpp b/src/metrics/tests/functionality_test.cpp index 35fafbd4..8cd9b357 100644 --- a/src/metrics/tests/functionality_test.cpp +++ b/src/metrics/tests/functionality_test.cpp @@ -23,10 +23,10 @@ #include #include -#include "logging/logging.h" +#include -#include "../metrics.hpp" -#include "../metrics_group_impl.hpp" +#include "sisl/metrics/metrics.hpp" +#include "sisl/metrics/metrics_group_impl.hpp" constexpr size_t ITERATIONS{2}; diff --git a/src/metrics/tests/metrics_benchmark.cpp b/src/metrics/tests/metrics_benchmark.cpp index 611cf0be..8d06c361 100644 --- a/src/metrics/tests/metrics_benchmark.cpp +++ b/src/metrics/tests/metrics_benchmark.cpp @@ -14,12 +14,13 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include #include -#include "metrics.hpp" #include +#include #include +#include "sisl/metrics/metrics.hpp" + SISL_LOGGING_INIT(vmod_metrics_framework) RCU_REGISTER_INIT diff --git a/src/metrics/tests/wrapper_test.cpp b/src/metrics/tests/wrapper_test.cpp index c22eeb84..1a505d11 100644 --- a/src/metrics/tests/wrapper_test.cpp +++ b/src/metrics/tests/wrapper_test.cpp @@ -18,9 +18,11 @@ #include #include #include -#include "metrics.hpp" + #include -#include "options/options.h" +#include + +#include "sisl/metrics/metrics.hpp" SISL_LOGGING_INIT(vmod_metrics_framework) @@ -216,7 +218,7 @@ nlohmann::json expected = { {"Total memory utilization", 980} }}, {"Histograms percentiles (usecs) avg/50/95/99", { - {"Distribution of request per transactions", "18.25 / 15.0 / 31.0 / 31.0"} + {"Distribution of request per transactions", "18.2 / 15.0 / 31.0 / 31.0"} }} }} }} diff --git a/src/options/CMakeLists.txt b/src/options/CMakeLists.txt index f181f962..eca57063 100644 --- a/src/options/CMakeLists.txt +++ b/src/options/CMakeLists.txt @@ -1,28 +1,18 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -include_directories(BEFORE include) - -file(GLOB API_HEADERS include/*.h) -file(GLOB LIB_HEADERS lib/*.h) -file(GLOB LIB_SOURCES lib/*.cpp) - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -add_library(sisl_options OBJECT - ${API_HEADERS} - ${LIB_HEADERS} - ${LIB_SOURCES} - ) +add_library(sisl_options OBJECT) +target_sources(sisl_options PRIVATE + options.cpp + ) target_link_libraries(sisl_options ${COMMON_DEPS}) -set(BASIC_TEST_SOURCES tests/basic.cpp) -add_executable(basic_test ${BASIC_TEST_SOURCES}) -target_link_libraries(basic_test sisl ${COMMON_DEPS} GTest::gtest) -set(extra_args "") -if (DEFINED CONAN_BUILD_COVERAGE) - if (${CONAN_BUILD_COVERAGE}) - set(extra_args "--gtest_output=xml:/output/test_basic.xml") - endif () -endif () -add_test(NAME BasicTest COMMAND basic_test ${extra_args}) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(basic_test) + target_sources(basic_test PRIVATE + tests/basic.cpp + ) + target_link_libraries(basic_test sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME OptionsBasics COMMAND basic_test ${extra_args}) + endif() +endif() diff --git a/src/options/lib/options.cpp b/src/options/options.cpp similarity index 92% rename from src/options/lib/options.cpp rename to src/options/options.cpp index f25c8a4f..545c21d1 100644 --- a/src/options/lib/options.cpp +++ b/src/options/options.cpp @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,6 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include "options/options.h" +#include SISL_OPTION_GROUP(main, (help, "h", "help", "Help message", ::cxxopts::value< bool >(), "")) diff --git a/src/options/tests/basic.cpp b/src/options/tests/basic.cpp index c24cbc0d..5284ef49 100644 --- a/src/options/tests/basic.cpp +++ b/src/options/tests/basic.cpp @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ #include #include -#include "options.h" +#include SISL_OPTION_GROUP(logging, (verbosity, "v", "verbosity", "Verbosity level (0-5)", diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt index 4c5999a4..6d240965 100644 --- a/src/settings/CMakeLists.txt +++ b/src/settings/CMakeLists.txt @@ -1,29 +1,33 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-attributes") # needed for C++ 20 folly compilation -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) -include_directories(BEFORE . ${CMAKE_CURRENT_BINARY_DIR}/generated/) - -find_package(FlatBuffers REQUIRED) +find_package(flatbuffers QUIET REQUIRED) -set(SETTINGS_SOURCE_FILES +add_library(sisl_settings OBJECT) +target_sources(sisl_settings PRIVATE settings.cpp - ) -add_library(sisl_settings OBJECT ${SETTINGS_SOURCE_FILES}) + ) target_link_libraries(sisl_settings ${COMMON_DEPS} flatbuffers::flatbuffers ) -set(TEST_SETTINGS_SOURCE_FILES - tests/test_settings.cpp - ) -add_executable(test_settings ${TEST_SETTINGS_SOURCE_FILES}) -settings_gen_cpp(${FLATBUFFERS_FLATC_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/generated/ test_settings tests/test_app_schema.fbs) -target_link_libraries(test_settings sisl ${COMMON_DEPS} flatbuffers::flatbuffers) -add_test(NAME SettingsTest COMMAND test_settings) -add_test(NAME SettingsTestOverride COMMAND test_settings --config_path=${CMAKE_BINARY_DIR} --override_config test_app_schema.config.database.databaseHost:myhost.com) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_settings) + target_sources(test_settings PRIVATE + tests/test_settings.cpp + ) + settings_gen_cpp( + ${FLATBUFFERS_FLATC_EXECUTABLE} + ${CMAKE_CURRENT_BINARY_DIR}/generated/ + test_settings + tests/test_app_schema.fbs + ) + if(NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + target_include_directories(test_settings BEFORE PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + endif() + target_include_directories(test_settings BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(test_settings sisl ${COMMON_DEPS} flatbuffers::flatbuffers GTest::gtest) + add_test(NAME Settings COMMAND test_settings) + endif() +endif() diff --git a/src/settings/README.md b/src/settings/README.md index 8c5f8f99..89641d29 100644 --- a/src/settings/README.md +++ b/src/settings/README.md @@ -75,7 +75,7 @@ settings_gen_cpp(${FLATBUFFERS_FLATC_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/gen In your main include code or separate code, add the following lines outside your namespace definition ```c++ -#include +#include #include "generated/homeblks_config_generated.h" // <--- Format is diff --git a/src/settings/settings.cpp b/src/settings/settings.cpp index 3e92dab0..2221810d 100644 --- a/src/settings/settings.cpp +++ b/src/settings/settings.cpp @@ -22,9 +22,9 @@ #include -#include "options/options.h" +#include -#include "settings.hpp" +#include "sisl/settings/settings.hpp" SISL_OPTION_GROUP(config, (config_path, "", "config_path", "Path to dynamic config of app", cxxopts::value< std::string >(), @@ -50,38 +50,46 @@ static nlohmann::json kv_path_to_json(const std::vector< std::string >& paths, c return nlohmann::json::parse(json_str); } -SettingsFactoryRegistry::SettingsFactoryRegistry() { +SettingsFactoryRegistry::SettingsFactoryRegistry(const std::string& path, + const std::vector< std::string >& override_cfgs) : + m_config_path{path} { + if (SISL_OPTIONS.count("config_path") != 0) { m_config_path = SISL_OPTIONS["config_path"].as< std::string >(); } + + std::vector< std::string > cfgs; if (SISL_OPTIONS.count("override_config") != 0) { - const auto cfgs{SISL_OPTIONS["override_config"].as< std::vector< std::string > >()}; - for (const auto& cfg : cfgs) { - // Get the entire path along with module name and its value - std::vector< std::string > kv; - boost::split(kv, cfg, boost::is_any_of(":=")); - if (kv.size() < 2) { continue; } - - // Split this and convert to a json string which json library can parse. I am sure - // there are cuter ways to do this, but well someother time.... - std::vector< std::string > paths; - boost::split(paths, kv[0], boost::is_any_of(".")); - if (paths.size() < 2) { continue; } - auto schema_name{std::move(paths.front())}; - paths.erase(std::begin(paths)); - - auto j = kv_path_to_json(paths, kv[1]); // Need a copy constructor here. - const auto it{m_override_cfgs.find(schema_name)}; - if (it != std::cend(m_override_cfgs)) { - it->second.merge_patch(j); - } else { - m_override_cfgs.emplace(std::move(schema_name), std::move(j)); - } + cfgs = SISL_OPTIONS["override_config"].as< std::vector< std::string > >(); + } else { + cfgs = override_cfgs; + } + + for (const auto& cfg : cfgs) { + // Get the entire path along with module name and its value + std::vector< std::string > kv; + boost::split(kv, cfg, boost::is_any_of(":=")); + if (kv.size() < 2) { continue; } + + // Split this and convert to a json string which json library can parse. I am sure + // there are cuter ways to do this, but well someother time.... + std::vector< std::string > paths; + boost::split(paths, kv[0], boost::is_any_of(".")); + if (paths.size() < 2) { continue; } + auto schema_name{std::move(paths.front())}; + paths.erase(std::begin(paths)); + + auto j = kv_path_to_json(paths, kv[1]); // Need a copy constructor here. + const auto it{m_override_cfgs.find(schema_name)}; + if (it != std::cend(m_override_cfgs)) { + it->second.merge_patch(j); + } else { + m_override_cfgs.emplace(std::move(schema_name), std::move(j)); } } } -void SettingsFactoryRegistry::register_factory(const std::string& name, SettingsFactoryBase* const f) { - if (SISL_OPTIONS.count("config_path") == 0) { return; } +void SettingsFactoryRegistry::register_factory(const std::string& name, SettingsFactoryBase* f) { + if (m_config_path.empty()) { return; } - const auto config_file{fmt::format("{}/{}.json", SISL_OPTIONS["config_path"].as< std::string >(), name)}; + const auto config_file{fmt::format("{}/{}.json", m_config_path, name)}; { std::unique_lock lg{m_mtx}; f->set_config_file(config_file); diff --git a/src/settings/tests/test_app_schema.fbs b/src/settings/tests/test_app_schema.fbs index b72c60ed..feceb2b6 100644 --- a/src/settings/tests/test_app_schema.fbs +++ b/src/settings/tests/test_app_schema.fbs @@ -1,4 +1,4 @@ -native_include "utility/non_null_ptr.hpp"; +native_include "sisl/utility/non_null_ptr.hpp"; namespace testapp; diff --git a/src/settings/tests/test_settings.cpp b/src/settings/tests/test_settings.cpp index b16c5fc9..0354b83c 100644 --- a/src/settings/tests/test_settings.cpp +++ b/src/settings/tests/test_settings.cpp @@ -14,28 +14,126 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include -#include - +#include +#include #include -#include "logging/logging.h" -#include "options/options.h" - -#include "test_app_schema_generated.h" -//#include "generated/test_app_schema_bindump.hpp" -#include "settings/settings.hpp" -#define MY_SETTINGS_FACTORY SETTINGS_FACTORY(test_app_schema) +#include +#include +#include "generated/test_app_schema_generated.h" +#include "sisl/settings/settings.hpp" SISL_OPTIONS_ENABLE(logging, test_settings, config) +SISL_LOGGING_INIT(test_settings, settings) +SETTINGS_INIT(testapp::TestAppSettings, test_app_schema) SISL_OPTION_GROUP(test_settings, (num_threads, "", "num_threads", "number of threads", ::cxxopts::value< uint32_t >()->default_value("1"), "number")) -SISL_LOGGING_INIT(test_settings, settings) -SETTINGS_INIT(testapp::TestAppSettings, test_app_schema) +static const char* g_schema_file{"/tmp/test_app_schema.json"}; + +class SettingsTest : public ::testing::Test { +protected: + void SetUp() override { std::remove(g_schema_file); } + + void init(const std::vector< std::string >& override_cfgs = {}) { + auto reg_mem = &sisl::SettingsFactoryRegistry::instance(); + sisl::SettingsFactoryRegistry::instance().~SettingsFactoryRegistry(); + new (reg_mem) sisl::SettingsFactoryRegistry("/tmp", override_cfgs); + + auto fac_mem = &test_app_schema_factory::instance(); + sisl::SettingsFactoryRegistry::instance().unregister_factory("test_app_schema"); + test_app_schema_factory::instance().~test_app_schema_factory(); + new (fac_mem) test_app_schema_factory(); + } + + void Teardown() { + test_app_schema_factory::instance().~test_app_schema_factory(); + sisl::SettingsFactoryRegistry::instance().~SettingsFactoryRegistry(); + std::remove(g_schema_file); + } +}; + +TEST_F(SettingsTest, LoadReload) { + init(); + + LOGINFO("Step 1: Validating default load"); + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->dbconnection->dbConnectionOptimalLoad), 100UL) + << "Incorrect load of dbConnectionOptimalLoad - default load"; + SETTINGS(test_app_schema, s, { + ASSERT_EQ(s.config.database.databaseHost, "") << "Incorrect load of databaseHost - default load"; + ASSERT_EQ(s.config.database.databasePort, 27017u) << "Incorrect load of databasePort - default load"; + ASSERT_EQ(s.config.database.numThreads, 8u) << "Incorrect load of numThreads - default load"; + }); + sisl::SettingsFactoryRegistry::instance().save_all(); + ASSERT_EQ(std::filesystem::exists("/tmp/test_app_schema.json"), true) << "Expect settings save to create the file"; + ASSERT_EQ(sisl::SettingsFactoryRegistry::instance().reload_all(), false) + << "Incorrectly asking to reload when hotswap variable is changed and reloaded"; + + LOGINFO("Step 2: Reload by dumping the settings to json, edit hotswap variable and reload it"); + nlohmann::json j = nlohmann::json::parse(SETTINGS_FACTORY(test_app_schema).get_json()); + j["config"]["dbconnection"]["dbConnectionOptimalLoad"] = 800; + ASSERT_EQ(SETTINGS_FACTORY(test_app_schema).reload_json(j.dump()), false) + << "Incorrectly asking restart when hotswap variable is changed and reloaded"; + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->dbconnection->dbConnectionOptimalLoad), 800UL) + << "Incorrect load of dbConnectionOptimalLoad - after reload json"; + SETTINGS(test_app_schema, s, { + ASSERT_EQ(s.config.database.databaseHost, "") << "Incorrect load of databaseHost - after reload json"; + ASSERT_EQ(s.config.database.databasePort, 27017u) << "Incorrect load of databasePort - after reload json"; + ASSERT_EQ(s.config.database.numThreads, 8u) << "Incorrect load of numThreads - after reload json"; + }); + LOGINFO("Step 3: Reload by dumping the settings to json, edit non-hotswap variable and dump to file and reload " + "settings"); + j = nlohmann::json::parse(SETTINGS_FACTORY(test_app_schema).get_json()); + j["config"]["database"]["databasePort"] = 25000u; + { + std::ofstream file(g_schema_file); + file << j; + } + ASSERT_EQ(sisl::SettingsFactoryRegistry::instance().reload_all(), true) + << "Incorrectly marking no restart when non-hotswap variable is changed and reloaded"; + + LOGINFO("Step 4: Simulate the app restart and validate new values"); + init(); + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->dbconnection->dbConnectionOptimalLoad), 800UL) + << "Incorrect load of dbConnectionOptimalLoad - after restart"; + SETTINGS(test_app_schema, s, { + ASSERT_EQ(s.config.database.databaseHost, "") << "Incorrect load of databaseHost - after reload json"; + ASSERT_EQ(s.config.database.databasePort, 25000u) << "Incorrect load of databasePort - after reload json"; + ASSERT_EQ(s.config.database.numThreads, 8u) << "Incorrect load of numThreads - after reload json"; + }); + sisl::SettingsFactoryRegistry::instance().save_all(); +} + +TEST_F(SettingsTest, OverrideConfig) { + LOGINFO("Step 1: Validating overridden config load"); + init({"test_app_schema.config.database.databaseHost:myhost.com", + "test_app_schema.config.glog.FLAGS_logbuflevel:100"}); + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->database->databaseHost), "myhost.com") + << "Incorrect load of databaseHost with override config"; + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->glog->FLAGS_logbuflevel), 100) + << "Incorrect load of FLAGS_logbuflevel with override config"; + SETTINGS(test_app_schema, s, { + ASSERT_EQ(s.config.database.databasePort, 27017u) << "Incorrect load of databasePort - default load"; + ASSERT_EQ(s.config.database.numThreads, 8u) << "Incorrect load of numThreads - default load"; + }); + sisl::SettingsFactoryRegistry::instance().save_all(); + + LOGINFO("Step 2: Simulate restart and default load saved the previously overridden config"); + init(); + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->database->databaseHost), "myhost.com") + << "Incorrect load of databaseHost with override config"; + ASSERT_EQ(SETTINGS_VALUE(test_app_schema, config->glog->FLAGS_logbuflevel), 100) + << "Incorrect load of FLAGS_logbuflevel with override config"; + SETTINGS(test_app_schema, s, { + ASSERT_EQ(s.config.database.databasePort, 27017u) << "Incorrect load of databasePort - default load"; + ASSERT_EQ(s.config.database.numThreads, 8u) << "Incorrect load of numThreads - default load"; + }); +} + +#if 0 int main(int argc, char* argv[]) { SISL_OPTIONS_LOAD(argc, argv, logging, test_settings, config); sisl::logging::SetLogger("test_settings"); @@ -81,3 +179,14 @@ int main(int argc, char* argv[]) { MY_SETTINGS_FACTORY.save("/tmp/settings_out"); return 0; } +#endif + +int main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + SISL_OPTIONS_LOAD(argc, argv, logging, test_settings) + sisl::logging::SetLogger("test_settings"); + spdlog::set_pattern("[%D %T%z] [%^%L%$] [%t] %v"); + + auto ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/src/sisl_version/CMakeLists.txt b/src/sisl_version/CMakeLists.txt deleted file mode 100644 index f6aab51a..00000000 --- a/src/sisl_version/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -cmake_minimum_required (VERSION 3.10) - -if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) - add_flags("-Wno-attributes") # needed for C++ 20 folly compilation -endif() - -include_directories(BEFORE ..) -include_directories(BEFORE .) - -set(VERSION_SOURCE_FILES - version.cpp - ) -add_library(sisl_version OBJECT ${VERSION_SOURCE_FILES}) -target_link_libraries(sisl_version ${COMMON_DEPS} zmarok-semver::zmarok-semver) -target_include_directories(sisl_version PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -set(TEST_VERSION_SOURCE_FILES - tests/test_version.cpp - ) -add_executable(test_version ${TEST_VERSION_SOURCE_FILES}) -target_link_libraries(test_version sisl ${COMMON_DEPS} zmarok-semver::zmarok-semver GTest::gtest) -add_test(NAME VersionTest COMMAND test_version) diff --git a/src/sobject/CMakeLists.txt b/src/sobject/CMakeLists.txt new file mode 100644 index 00000000..95c60b8f --- /dev/null +++ b/src/sobject/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required (VERSION 3.11) + +add_library(sisl_sobject_mgr OBJECT) +target_sources(sisl_sobject_mgr PRIVATE sobject.cpp) +target_link_libraries(sisl_sobject_mgr ${COMMON_DEPS}) + +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_sobject) + target_sources(test_sobject PRIVATE + tests/test_sobject.cpp + ) + target_link_libraries(test_sobject sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME Sobject COMMAND test_sobject) + endif() +endif() diff --git a/src/sobject/sobject.cpp b/src/sobject/sobject.cpp new file mode 100644 index 00000000..5d892c1b --- /dev/null +++ b/src/sobject/sobject.cpp @@ -0,0 +1,200 @@ +/********************************************************************************* + * Modifications Copyright 2017-2019 eBay Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include "sisl/logging/logging.h" +#include "sisl/sobject/sobject.hpp" + +namespace sisl { + +sobject_ptr sobject::get_child(const std::string& name) { + std::shared_lock lock{m_mtx}; + auto iter = m_children.find(name); + if (iter == m_children.end()) { return nullptr; } + return iter->second; +} + +void sobject::add_child(const sobject_ptr child) { + // Add a child to current object. + std::unique_lock lock{m_mtx}; + LOGINFO("Parent {}/{} added child {}/{}", type(), name(), child->type(), child->name()); + m_children.emplace(child->name(), child); + m_mgr->add_object_type(type(), child->type()); +} + +void sobject::add_child_type(const std::string& child_type) { + std::unique_lock lock{m_mtx}; + LOGINFO("Added type parent {} child {}", type(), child_type); + m_mgr->add_object_type(type(), child_type); +} + +status_response sobject::run_callback(const status_request& request) const { + status_response response; + response.json = nlohmann::json::object(); + response.json["type"] = m_type; + response.json["name"] = m_name; + auto res = m_status_cb(request).json; + if (!res.is_null()) { response.json.update(res); } + + for (const auto& [name, obj] : m_children) { + auto child_type = obj->type(); + auto child_name = obj->name(); + if (response.json["children"] == nullptr) { response.json["children"] = nlohmann::json::object(); } + + if (response.json["children"][child_type] == nullptr) { + response.json["children"][child_type] == nlohmann::json::array(); + } + + if (request.do_recurse) { + // Call recursive. + auto child_json = obj->run_callback(request).json; + response.json["children"][child_type].emplace_back(child_json); + } else { + response.json["children"][child_type].emplace_back(child_name); + } + } + + return response; +} + +sobject_ptr sobject_manager::create_object(const std::string& type, const std::string& name, status_callback_type cb) { + std::unique_lock lock{m_mtx}; + auto obj = sobject::create(this, type, name, std::move(cb)); + m_object_store[name] = obj; + if (m_object_types.count(type) == 0) { m_object_types[type] = {}; } + LOGINFO("Created status object type={} name={}", type, name); + return obj; +} + +void sobject_manager::add_object_type(const std::string& parent_type, const std::string& child_type) { + std::unique_lock lock{m_mtx}; + m_object_types[parent_type].insert(child_type); +} + +status_response sobject_manager::get_object_types(const std::string& type) { + status_response response; + auto children = nlohmann::json::object(); + for (const auto& child : m_object_types[type]) { + children.emplace(child, get_object_types(child).json); + } + response.json = children; + return response; +} + +status_response sobject_manager::get_objects(const status_request& request) { + status_response response; + + // We by default start from the 'module' types recursively as they are + // the top of the heirarchy. + std::string obj_type = request.obj_type.empty() ? "module" : request.obj_type; + auto iter = m_object_store.begin(); + if (!request.next_cursor.empty()) { + // Extract cursor which has name. + iter = m_object_store.find(request.next_cursor); + if (iter == m_object_store.end()) return status_error("Cursor not found"); + } + + int batch_size = request.batch_size; + while (iter != m_object_store.end() && batch_size > 0) { + if (obj_type != iter->second->type()) { + iter++; + continue; + } + + response.json[iter->first] = iter->second->run_callback(request).json; + iter++; + batch_size--; + } + + if (iter != m_object_store.end() && obj_type == iter->second->type()) { + response.json["next_cursor"] = iter->second->name(); + } + + return response; +} + +status_response sobject_manager::get_object_status(const std::string& name, const status_request& request) { + auto iter = m_object_store.find(name); + if (iter == m_object_store.end()) { return status_error("Object identifier not found"); } + return iter->second->run_callback(request); +} + +status_response sobject_manager::get_child_type_status( const status_request& request) { + status_response response; + auto iter = m_object_store.find(request.obj_name); + if (iter == m_object_store.end()) { return status_error("Object identifier not found"); } + for (const auto& [child_name, child_obj] : iter->second->m_children) { + if (child_obj->type() == request.obj_type) { + response.json[child_name] = child_obj->run_callback(request).json; + } + } + if (!response.json.empty()) { + // If we found child in object tree return it. + return response; + } + + // Else ask the parent object to do the work. This is used to lazily + // get objects of type which are not created by default. + return iter->second->run_callback(request); +} + +status_response sobject_manager::get_object_by_path(const status_request& request) { + sobject_ptr obj = nullptr; + auto iter = m_object_store.find(request.obj_path[0]); + if (iter == m_object_store.end()) { return status_error("Object identifier not found"); } + obj = iter->second; + for (uint32_t ii = 1; ii < request.obj_path.size(); ii++) { + obj = obj->get_child(request.obj_path[ii]); + if (obj == nullptr) { return status_error("Object identifier not found"); } + } + return obj->run_callback(request); +} + +status_response sobject_manager::get_status(const status_request& request) { + std::shared_lock lock{m_mtx}; + + if (!request.obj_path.empty()) { + // Return object status by path. + return get_object_by_path(request); + } + + if (!request.obj_type.empty() && !request.obj_name.empty()) { + // Get all children under the parent of type. + return get_child_type_status(request); + } + + if (!request.obj_name.empty()) { + // Return specific object. + return get_object_status(request.obj_name, request); + } + + if (!request.obj_type.empty()) { + // Return all objects of this type. + return get_objects(request); + } + + if (!request.do_recurse) { + // If no recurse we only return the types. + status_response response; + response.json["module"] = get_object_types("module").json; + return response; + } + + // Dump all objects recursively. + return get_objects(request); +} + +} // namespace sisl diff --git a/src/sobject/tests/test_sobject.cpp b/src/sobject/tests/test_sobject.cpp new file mode 100644 index 00000000..7fdd0752 --- /dev/null +++ b/src/sobject/tests/test_sobject.cpp @@ -0,0 +1,153 @@ +/********************************************************************************* + * Modifications Copyright 2023 eBay Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "sisl/sobject/sobject.hpp" + +using namespace sisl; +using namespace std; + +SISL_LOGGING_INIT(test_sobject) +SISL_OPTIONS_ENABLE(logging) + +namespace { + +class SobjectTest : public testing::Test { +public: + SobjectTest() : testing::Test() {} + + sobject_manager mgr; + +protected: + void SetUp() override {} + void TearDown() override {} +}; + +TEST_F(SobjectTest, BasicTest) { + auto create_nodes = [this](sobject_ptr parent, string type, string prefix, int count) { + vector< sobject_ptr > res; + for (int i = 1; i <= count; i++) { + auto n = prefix + "_" + to_string(i); + auto cb = [n](const status_request&) { + status_response resp; + resp.json[n + "_metric"] = 1; + return resp; + }; + + auto o = mgr.create_object(type, n, cb); + res.push_back(o); + if (parent) { parent->add_child(o); } + } + return res; + }; + + // Create heirarchy of objects. + auto module_vec = create_nodes(nullptr, "module", "module", 3); + auto a_vec = create_nodes(module_vec[0], "A", "A", 2); + auto b_vec = create_nodes(module_vec[1], "B", "B", 2); + auto c_vec = create_nodes(module_vec[0], "C", "C", 2); + + auto a_sub_vec = create_nodes(a_vec[0], "A_sub", "A_sub", 2); + auto b_sub_vec = create_nodes(b_vec[0], "B_sub", "B_sub", 2); + auto c_sub_vec = create_nodes(c_vec[0], "C_sub", "C_sub", 2); + + auto c_child_child_vec = create_nodes(c_sub_vec[0], "C_sub_sub", "C_sub_sub", 2); + + + { + // Get all objects. + status_request req; + status_response resp; + resp = mgr.get_status(req); + LOGINFO("{}", resp.json.dump()); + ASSERT_EQ(resp.json.dump(), R"({"module":{"A":{"A_sub":{}},"B":{"B_sub":{}},"C":{"C_sub":{"C_sub_sub":{}}}}})"); + } + + { + // Get object by name recursive and non recursive. + status_request req; + status_response resp; + req.obj_name = "module_1"; + req.do_recurse = true; + resp = mgr.get_status(req); + LOGINFO("{}", resp.json.dump()); + // TODO add validation. + + req.do_recurse = false; + resp = mgr.get_status(req); + LOGINFO("{}", resp.json.dump()); + ASSERT_EQ(resp.json.dump(), R"({"children":{"A":["A_1","A_2"],"C":["C_1","C_2"]},"module_1_metric":1,"name":"module_1","type":"module"})"); + } + + { + // Get object by type recursive and non recursive. + status_request req; + status_response resp; + req.do_recurse = true; + req.obj_type = "C"; + resp = mgr.get_status(req); + LOGINFO("{}", resp.json.dump()); + } + + { + status_request req; + status_response resp; + req.obj_path = {"module_1", "C_1", "C_sub_1", "C_sub_sub_1"}; + req.do_recurse = false; + resp = mgr.get_status(req); + LOGINFO("Response {}", resp.json.dump(2)); + ASSERT_EQ(resp.json["name"], "C_sub_sub_1") << resp.json.dump(); + ASSERT_EQ(resp.json["type"], "C_sub_sub") << resp.json.dump(); + } + + { + status_request req; + status_response resp; + auto d_vec = create_nodes(nullptr, "D", "D", 10); + req.do_recurse = true; + req.batch_size = 1; + req.obj_type = "D"; + auto count = 10; + while (true) { + resp = mgr.get_status(req); + count--; + LOGINFO("Response {}", resp.json.dump()); + if (!resp.json.contains("next_cursor")) break; + req.next_cursor = resp.json["next_cursor"]; + } + + ASSERT_EQ(count, 0) << resp.json.dump(2); + } +} +} // namespace + +int main(int argc, char* argv[]) { + SISL_OPTIONS_LOAD(argc, argv, logging) + ::testing::InitGoogleTest(&argc, argv); + sisl::logging::SetLogger("test_sobject"); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + const auto ret{RUN_ALL_TESTS()}; + return ret; +} diff --git a/src/utility/CMakeLists.txt b/src/utility/CMakeLists.txt index ea33be93..fc41362d 100644 --- a/src/utility/CMakeLists.txt +++ b/src/utility/CMakeLists.txt @@ -1,43 +1,44 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -add_flags("-Wno-unused-parameter") +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) -include_directories(BEFORE ..) -include_directories(BEFORE .) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_atomic_counter) + target_sources(test_atomic_counter PRIVATE + tests/test_atomic_counter.cpp + ) + target_link_libraries(test_atomic_counter sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME AtomicCounter COMMAND test_atomic_counter) -set(TEST_ATOMIC_COUNTER_SOURCES - tests/test_atomic_counter.cpp - ) -add_executable(test_atomic_counter ${TEST_ATOMIC_COUNTER_SOURCES}) -target_link_libraries(test_atomic_counter sisl ${COMMON_DEPS} GTest::gtest) -add_test(NAME atomic_counter COMMAND test_atomic_counter) + add_executable(test_thread_buffer) + target_sources(test_thread_buffer PRIVATE + tests/test_thread_buffer.cpp + ) + target_link_libraries(test_thread_buffer ${COMMON_DEPS} GTest::gtest) + add_test(NAME ThreadBuffer COMMAND test_thread_buffer) -set(TEST_THREAD_BUFFER - tests/test_thread_buffer.cpp - ) -add_executable(test_thread_buffer ${TEST_THREAD_BUFFER}) -target_link_libraries(test_thread_buffer ${COMMON_DEPS} GTest::gtest) -add_test(NAME ThreadBufferTest COMMAND test_thread_buffer) + add_executable(test_status_factory) + target_sources(test_status_factory PRIVATE + tests/test_status_factory.cpp + ) + target_link_libraries(test_status_factory ${COMMON_DEPS} benchmark::benchmark) + add_test(NAME StatusFactory COMMAND test_status_factory) -set(TEST_STATUS_FACTORY - tests/test_status_factory.cpp - ) -add_executable(test_status_factory ${TEST_STATUS_FACTORY}) -target_link_libraries(test_status_factory ${COMMON_DEPS} benchmark::benchmark) -add_test(NAME StatusFactoryTest COMMAND test_status_factory) + add_executable(test_enum) + target_sources(test_enum PRIVATE + tests/test_enum.cpp + ) + target_link_libraries(test_enum ${COMMON_DEPS} GTest::gtest) + add_test(NAME Enum COMMAND test_enum) -set(TEST_ENUM - tests/test_enum.cpp - ) -add_executable(test_enum ${TEST_ENUM}) -target_link_libraries(test_enum ${COMMON_DEPS} GTest::gtest) -add_test(NAME EnumTest COMMAND test_enum) - -if (${prerelease_dummy_FOUND}) - set(TEST_OBJLIFE - tests/test_objlife_counter.cpp - ) - add_executable(test_objlife ${TEST_OBJLIFE}) - target_link_libraries(test_objlife sisl ${COMMON_DEPS} GTest::gtest) - add_test(NAME ObjLifeTest COMMAND test_objlife) -endif () + if (${prerelease_dummy_FOUND}) + add_executable(test_objlife) + target_sources(test_objlife PRIVATE + tests/test_objlife_counter.cpp + ) + target_link_libraries(test_objlife sisl ${COMMON_DEPS} GTest::gtest) + add_test(NAME ObjLife COMMAND test_objlife) + endif () + endif() +endif() diff --git a/src/utility/tests/test_atomic_counter.cpp b/src/utility/tests/test_atomic_counter.cpp index 066b0293..f627eb37 100644 --- a/src/utility/tests/test_atomic_counter.cpp +++ b/src/utility/tests/test_atomic_counter.cpp @@ -5,12 +5,12 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include #include -#include "atomic_counter.hpp" +#include "sisl/utility/atomic_counter.hpp" using namespace sisl; diff --git a/src/utility/tests/test_enum.cpp b/src/utility/tests/test_enum.cpp index 3155b179..c01ef0c7 100644 --- a/src/utility/tests/test_enum.cpp +++ b/src/utility/tests/test_enum.cpp @@ -9,8 +9,7 @@ #include -#include "thread_buffer.hpp" -#include "utility/enum.hpp" +#include "sisl/utility/enum.hpp" class EnumTest : public testing::Test { public: @@ -65,7 +64,7 @@ TEST_F(EnumTest, enum_unsigned_test) { ASSERT_EQ(enum_name(unsigned_enum::val2), "val2"); } -ENUM(signed_enum_value, int16_t, val1=-10, val2=-20) +ENUM(signed_enum_value, int16_t, val1 = -10, val2 = -20) TEST_F(EnumTest, enum_signed_value_test) { auto enum_lambda{[](const signed_enum_value& val) { switch (val) { @@ -84,7 +83,7 @@ TEST_F(EnumTest, enum_signed_value_test) { ASSERT_EQ(enum_name(signed_enum_value::val2), "val2"); } -ENUM(unsigned_enum_value, uint16_t, val1=10, val2=20, val3=1<<4, val4 = +30, val5 = 40u) +ENUM(unsigned_enum_value, uint16_t, val1 = 10, val2 = 20, val3 = 1 << 4, val4 = +30, val5 = 40u) TEST_F(EnumTest, enum_unsigned_value_test) { auto enum_lambda{[](const unsigned_enum_value& val) { switch (val) { @@ -113,11 +112,6 @@ TEST_F(EnumTest, enum_unsigned_value_test) { ASSERT_EQ(enum_name(unsigned_enum_value::val3), "val3"); ASSERT_EQ(enum_name(unsigned_enum_value::val4), "val4"); ASSERT_EQ(enum_name(unsigned_enum_value::val5), "val5"); - //ASSERT_EQ(enum_value("val1"), unsigned_enum_value::val1); - //ASSERT_EQ(enum_value("val2"), unsigned_enum_value::val2); - //ASSERT_EQ(enum_value("val3"), unsigned_enum_value::val3); - //ASSERT_EQ(enum_value("val4"), unsigned_enum_value::val4); - //ASSERT_EQ(enum_value("val5"), unsigned_enum_value::val5); } ENUM(signed_enum_mixed, int16_t, val1 = -10, val2) @@ -137,11 +131,9 @@ TEST_F(EnumTest, enum_signed_mixed_test) { ASSERT_EQ(enum_lambda(signed_enum_mixed::val2), -9); ASSERT_EQ(enum_name(signed_enum_mixed::val1), "val1"); ASSERT_EQ(enum_name(signed_enum_mixed::val2), "val2"); - //ASSERT_EQ(enum_value("val1"), signed_enum_mixed::val1); - //ASSERT_EQ(enum_value("val2") ,signed_enum_mixed::val2); } -ENUM(unsigned_enum_mixed, uint16_t, val1 = 10, val2, val3 = 1<<2) +ENUM(unsigned_enum_mixed, uint16_t, val1 = 10, val2, val3 = 1 << 2) TEST_F(EnumTest, enum_unsigned_mixed_test) { auto enum_lambda{[](const unsigned_enum_mixed& val) { switch (val) { @@ -162,12 +154,9 @@ TEST_F(EnumTest, enum_unsigned_mixed_test) { ASSERT_EQ(enum_name(unsigned_enum_mixed::val1), "val1"); ASSERT_EQ(enum_name(unsigned_enum_mixed::val2), "val2"); ASSERT_EQ(enum_name(unsigned_enum_mixed::val3), "val3"); - //ASSERT_EQ(enum_value("val1"), unsigned_enum_value::val1); - //ASSERT_EQ(enum_value("val2"), unsigned_enum_value::val2); - //ASSERT_EQ(enum_value("val3"), unsigned_enum_value::val3); } -ENUM(unsigned_enum2, uint16_t, val1=0x1, val2=0x2, val3=0x3) +ENUM(unsigned_enum2, uint16_t, val1 = 0x1, val2 = 0x2, val3 = 0x3) TEST_F(EnumTest, enum_unsigned_test_bit_ops) { auto enum_lambda{[](const unsigned_enum2& val) { switch (val) { @@ -188,9 +177,6 @@ TEST_F(EnumTest, enum_unsigned_test_bit_ops) { ASSERT_EQ(enum_name(unsigned_enum2::val1), "val1"); ASSERT_EQ(enum_name(unsigned_enum2::val2), "val2"); ASSERT_EQ(enum_name(unsigned_enum2::val3), "val3"); - //ASSERT_EQ(enum_value("val1"), unsigned_enum2::val1); - //ASSERT_EQ(enum_value("val2"), unsigned_enum2::val2); - //ASSERT_EQ(enum_value("val3"), unsigned_enum2::val3); ASSERT_EQ(unsigned_enum2::val1 | unsigned_enum2::val2, unsigned_enum2::val3); ASSERT_EQ(unsigned_enum2::val1 & unsigned_enum2::val3, unsigned_enum2::val1); @@ -202,6 +188,30 @@ TEST_F(EnumTest, enum_unsigned_test_bit_ops) { ASSERT_EQ(val2, unsigned_enum2::val2); } +class Container { +public: + SCOPED_ENUM_DECL(signed_enum, int16_t) +}; + +SCOPED_ENUM_DEF(Container, signed_enum, int16_t, val1, val2) +TEST_F(EnumTest, scoped_enum) { + auto enum_lambda{[](const Container::signed_enum& val) { + switch (val) { + case Container::signed_enum::val1: + return 1; + case Container::signed_enum::val2: + return 2; + default: + return 0; + }; + }}; + + ASSERT_EQ(enum_lambda(Container::signed_enum::val1), 1); + ASSERT_EQ(enum_lambda(Container::signed_enum::val2), 2); + ASSERT_EQ(enum_name(Container::signed_enum::val1), "val1"); + ASSERT_EQ(enum_name(Container::signed_enum::val2), "val2"); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/src/utility/tests/test_objlife_counter.cpp b/src/utility/tests/test_objlife_counter.cpp index 1b96a503..f9c0e721 100644 --- a/src/utility/tests/test_objlife_counter.cpp +++ b/src/utility/tests/test_objlife_counter.cpp @@ -7,12 +7,11 @@ #include #include -#include "logging/logging.h" -#include "options/options.h" +#include +#include -#include "fds/buffer.hpp" - -#include "obj_life_counter.hpp" +#include "sisl/fds/buffer.hpp" +#include "sisl/utility/obj_life_counter.hpp" SISL_LOGGING_INIT(test_objlife) diff --git a/src/utility/tests/test_status_factory.cpp b/src/utility/tests/test_status_factory.cpp index 9d170862..b0f3c0d1 100644 --- a/src/utility/tests/test_status_factory.cpp +++ b/src/utility/tests/test_status_factory.cpp @@ -1,10 +1,11 @@ #include -#include #include #include #include #include +#include "sisl/utility/urcu_helper.hpp" + RCU_REGISTER_INIT #define ITERATIONS 10000 diff --git a/src/utility/tests/test_thread_buffer.cpp b/src/utility/tests/test_thread_buffer.cpp index ce12c36c..e3cf4fb7 100644 --- a/src/utility/tests/test_thread_buffer.cpp +++ b/src/utility/tests/test_thread_buffer.cpp @@ -13,7 +13,7 @@ #include -#include "utility/thread_buffer.hpp" +#include "sisl/utility/thread_buffer.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/src/version/CMakeLists.txt b/src/version/CMakeLists.txt new file mode 100644 index 00000000..1df1dec6 --- /dev/null +++ b/src/version/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required (VERSION 3.11) + +add_library(sisl_version OBJECT) +target_sources(sisl_version PRIVATE + version.cpp + ) +target_link_libraries(sisl_version ${COMMON_DEPS} zmarok-semver::zmarok-semver) + +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(test_version) + target_sources(test_version PRIVATE + tests/test_version.cpp + ) + target_link_libraries(test_version sisl ${COMMON_DEPS} zmarok-semver::zmarok-semver GTest::gtest) + add_test(NAME Version COMMAND test_version) + endif() +endif() diff --git a/src/sisl_version/tests/test_version.cpp b/src/version/tests/test_version.cpp similarity index 61% rename from src/sisl_version/tests/test_version.cpp rename to src/version/tests/test_version.cpp index a432801f..ee8f5f2b 100644 --- a/src/sisl_version/tests/test_version.cpp +++ b/src/version/tests/test_version.cpp @@ -1,6 +1,6 @@ -#include -#include "logging/logging.h" -#include "options/options.h" +#include +#include +#include #include #include @@ -17,13 +17,15 @@ void entry() { TEST(entryTest, entry) { entry(); - const std::string dummy_ver{fmt::format("{0}", sisl::VersionMgr::getVersion("dummy"))}; - LOGINFO("Dummy ver. {}", dummy_ver); + std::stringstream dummy_ver; + dummy_ver << sisl::VersionMgr::getVersion("dummy"); + LOGINFO("Dummy ver. {}", dummy_ver.str()); - const std::string sisl_ver{fmt::format("{0}", sisl::VersionMgr::getVersion("sisl"))}; - LOGINFO("SISL ver. {}", sisl_ver); + std::stringstream sisl_ver; + sisl_ver << sisl::VersionMgr::getVersion("sisl"); + LOGINFO("SISL ver. {}", sisl_ver.str()); - EXPECT_EQ(dummy_ver, sisl_ver); + EXPECT_EQ(dummy_ver.str(), sisl_ver.str()); auto versions{sisl::VersionMgr::getVersions()}; EXPECT_EQ((int)versions.size(), 2); diff --git a/src/sisl_version/version.cpp b/src/version/version.cpp similarity index 97% rename from src/sisl_version/version.cpp rename to src/version/version.cpp index 4326b94b..90f74605 100644 --- a/src/sisl_version/version.cpp +++ b/src/version/version.cpp @@ -1,7 +1,7 @@ /********************************************************************************* * Modifications Copyright 2017-2019 eBay Inc. * - * Author/Developer(s): Brian Szymd + * Author/Developer(s): Brian Szmyd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * specific language governing permissions and limitations under the License. * *********************************************************************************/ -#include "version.hpp" +#include #include namespace sisl { diff --git a/src/wisr/CMakeLists.txt b/src/wisr/CMakeLists.txt index b3d601fe..bad69615 100644 --- a/src/wisr/CMakeLists.txt +++ b/src/wisr/CMakeLists.txt @@ -1,37 +1,38 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.11) -add_flags("-Wno-unused-parameter") +include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) -include_directories(BEFORE ..) -include_directories(BEFORE .) +if (DEFINED ENABLE_TESTING) + if (${ENABLE_TESTING}) + add_executable(wisr_vector_test) + target_sources(wisr_vector_test PRIVATE + tests/test_wisr_vector.cpp + ) + target_link_libraries(wisr_vector_test ${COMMON_DEPS} benchmark::benchmark GTest::gtest) + add_test(NAME WisrVector COMMAND wisr_vector_test) -set(WISR_VECTOR_TEST - tests/test_wisr_vector.cpp - ) -add_executable(wisr_vector_test ${WISR_VECTOR_TEST}) -target_link_libraries(wisr_vector_test ${COMMON_DEPS} benchmark::benchmark GTest::gtest) -add_test(NAME WisrVectorTest COMMAND wisr_vector_test) + add_executable(wisr_vector_benchmark) + target_sources(wisr_vector_benchmark PRIVATE + tests/wisr_vector_benchmark.cpp + ) + target_link_libraries(wisr_vector_benchmark ${COMMON_DEPS} benchmark::benchmark) -set(WISR_VECTOR_BENCHMARK - tests/wisr_vector_benchmark.cpp - ) -add_executable(wisr_vector_benchmark ${WISR_VECTOR_BENCHMARK}) -target_link_libraries(wisr_vector_benchmark ${COMMON_DEPS} benchmark::benchmark) + add_executable(wisr_list_benchmark) + target_sources(wisr_list_benchmark PRIVATE + tests/wisr_list_benchmark.cpp + ) + target_link_libraries(wisr_list_benchmark ${COMMON_DEPS} benchmark::benchmark) -set(WISR_LIST_BENCHMARK - tests/wisr_list_benchmark.cpp - ) -add_executable(wisr_list_benchmark ${WISR_LIST_BENCHMARK}) -target_link_libraries(wisr_list_benchmark ${COMMON_DEPS} benchmark::benchmark) + add_executable(wisr_deque_benchmark) + target_sources(wisr_deque_benchmark PRIVATE + tests/wisr_deque_benchmark.cpp + ) + target_link_libraries(wisr_deque_benchmark ${COMMON_DEPS} benchmark::benchmark) -set(WISR_DEQUE_BENCHMARK - tests/wisr_deque_benchmark.cpp - ) -add_executable(wisr_deque_benchmark ${WISR_DEQUE_BENCHMARK}) -target_link_libraries(wisr_deque_benchmark ${COMMON_DEPS} benchmark::benchmark) - -set(WISR_INTRUSIVE_SLIST_BENCHMARK - tests/wisr_intrusive_slist_benchmark.cpp - ) -add_executable(wisr_intrusive_slist_benchmark ${WISR_INTRUSIVE_SLIST_BENCHMARK}) -target_link_libraries(wisr_intrusive_slist_benchmark ${COMMON_DEPS} benchmark::benchmark) + add_executable(wisr_intrusive_slist_benchmark) + target_sources(wisr_intrusive_slist_benchmark PRIVATE + tests/wisr_intrusive_slist_benchmark.cpp + ) + target_link_libraries(wisr_intrusive_slist_benchmark ${COMMON_DEPS} benchmark::benchmark) + endif() +endif() diff --git a/src/wisr/tests/test_wisr_vector.cpp b/src/wisr/tests/test_wisr_vector.cpp index 383e1b93..4d2803eb 100644 --- a/src/wisr/tests/test_wisr_vector.cpp +++ b/src/wisr/tests/test_wisr_vector.cpp @@ -27,8 +27,8 @@ #include -#include "utility/thread_buffer.hpp" -#include "wisr/wisr_ds.hpp" +#include "sisl/utility/thread_buffer.hpp" +#include "sisl/wisr/wisr_ds.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/src/wisr/tests/wisr_deque_benchmark.cpp b/src/wisr/tests/wisr_deque_benchmark.cpp index e16d938d..70ec3649 100644 --- a/src/wisr/tests/wisr_deque_benchmark.cpp +++ b/src/wisr/tests/wisr_deque_benchmark.cpp @@ -21,8 +21,8 @@ #include -#include "utility/thread_buffer.hpp" -#include "wisr/wisr_ds.hpp" +#include "sisl/utility/thread_buffer.hpp" +#include "sisl/wisr/wisr_ds.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/src/wisr/tests/wisr_intrusive_slist_benchmark.cpp b/src/wisr/tests/wisr_intrusive_slist_benchmark.cpp index 87b382d2..d8ce6abf 100644 --- a/src/wisr/tests/wisr_intrusive_slist_benchmark.cpp +++ b/src/wisr/tests/wisr_intrusive_slist_benchmark.cpp @@ -24,8 +24,8 @@ #include -#include "utility/thread_buffer.hpp" -#include "wisr/wisr_ds.hpp" +#include "sisl/utility/thread_buffer.hpp" +#include "sisl/wisr/wisr_ds.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/src/wisr/tests/wisr_list_benchmark.cpp b/src/wisr/tests/wisr_list_benchmark.cpp index d2220050..633e5fef 100644 --- a/src/wisr/tests/wisr_list_benchmark.cpp +++ b/src/wisr/tests/wisr_list_benchmark.cpp @@ -20,8 +20,8 @@ #include -#include "utility/thread_buffer.hpp" -#include "wisr/wisr_ds.hpp" +#include "sisl/utility/thread_buffer.hpp" +#include "sisl/wisr/wisr_ds.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/src/wisr/tests/wisr_vector_benchmark.cpp b/src/wisr/tests/wisr_vector_benchmark.cpp index 3735d076..ed05ee07 100644 --- a/src/wisr/tests/wisr_vector_benchmark.cpp +++ b/src/wisr/tests/wisr_vector_benchmark.cpp @@ -21,8 +21,8 @@ #include -#include "utility/thread_buffer.hpp" -#include "wisr/wisr_ds.hpp" +#include "sisl/utility/thread_buffer.hpp" +#include "sisl/wisr/wisr_ds.hpp" THREAD_BUFFER_INIT RCU_REGISTER_INIT diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt new file mode 100644 index 00000000..4378dc15 --- /dev/null +++ b/test_package/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.11) +project(test_package) + +set(CMAKE_CXX_STANDARD 20) + +find_package(sisl QUIET REQUIRED) + +add_executable(${PROJECT_NAME} test_package.cpp example_decl.cpp) +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) +target_link_libraries(${PROJECT_NAME} sisl::sisl) diff --git a/test_package/conanfile.py b/test_package/conanfile.py new file mode 100644 index 00000000..075654ab --- /dev/null +++ b/test_package/conanfile.py @@ -0,0 +1,26 @@ +from conan import ConanFile +from conan.tools.build import can_run +from conan.tools.cmake import cmake_layout, CMake +import os + + +class TestPackageConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps", "CMakeToolchain", "VirtualRunEnv" + test_type = "explicit" + + def requirements(self): + self.requires(self.tested_reference_str) + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def test(self): + if can_run(self): + bin_path = os.path.join(self.cpp.build.bindir, "test_package") + self.run(bin_path, env="conanrun") diff --git a/test_package/example_decl.cpp b/test_package/example_decl.cpp new file mode 100644 index 00000000..2b560a84 --- /dev/null +++ b/test_package/example_decl.cpp @@ -0,0 +1,7 @@ +#include + +SISL_LOGGING_DEF(my_module) + +void example_decl() { + LOGINFOMOD(my_module, "Example def!"); +} diff --git a/test_package/test_package.cpp b/test_package/test_package.cpp new file mode 100644 index 00000000..de2800cd --- /dev/null +++ b/test_package/test_package.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include + +SISL_LOGGING_DECL(my_module) +SISL_LOGGING_INIT(my_module) + +SISL_OPTIONS_ENABLE(logging) + +extern void example_decl(); + +using namespace std::chrono_literals; + +[[ maybe_unused ]] +void crash() { volatile int* a = (int*)(NULL); *a = 1; } + +int main(int argc, char** argv) { + SISL_OPTIONS_LOAD(argc, argv, logging) + sisl::logging::SetLogger(std::string(argv[0])); + sisl::logging::install_crash_handler(); + spdlog::set_pattern("[%D %T%z] [%^%l%$] [%n] [%t] %v"); + + LOGTRACE("Trace"); + LOGDEBUG("Debug"); + LOGINFO("Info"); + LOGWARN("Warning"); + LOGERROR("Error"); + LOGCRITICAL("Critical"); + + auto _thread = std::thread([]() { + LOGWARNMOD(my_module, "Sleeping..."); + std::this_thread::sleep_for(1500ms); + LOGINFOMOD(my_module, "Waking..."); + std::this_thread::sleep_for(1500ms); + }); + sisl::name_thread(_thread, "example_decl"); + std::this_thread::sleep_for(300ms); + + auto custom_logger = + sisl::logging::CreateCustomLogger("test_package", "_custom", false /*stdout*/, true /*stderr*/); + LOGINFOMOD_USING_LOGGER(my_module, custom_logger, "hello world"); + DEBUG_ASSERT(true, "Always True"); + _thread.join(); + // crash(); + return 0; +}