mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-14 08:45:51 +00:00
Compare commits
121 Commits
release/1.
...
2.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aace437245 | ||
|
|
2e3e8cd779 | ||
|
|
6f93e1003e | ||
|
|
14978ca91d | ||
|
|
9adcaeb21b | ||
|
|
d94fe4e7ab | ||
|
|
c8029255ba | ||
|
|
d548d44a61 | ||
|
|
4cae248b5c | ||
|
|
b3db4cadab | ||
|
|
02c0a1f11d | ||
|
|
6bc2ec745f | ||
|
|
d7d5d61747 | ||
|
|
f1b3a6b511 | ||
|
|
f52f36ecbc | ||
|
|
860d10cddc | ||
|
|
36ac3215e2 | ||
|
|
7776a5ffb6 | ||
|
|
4b2d53fc2f | ||
|
|
9a19519550 | ||
|
|
88e25687dc | ||
|
|
93e2ac529d | ||
|
|
0bc84fefbf | ||
|
|
36bb20806e | ||
|
|
dfe974d5ab | ||
|
|
bf65cfabae | ||
|
|
f42e024f38 | ||
|
|
d816ef54ab | ||
|
|
e60fd3e58e | ||
|
|
654168efec | ||
|
|
5d06a79f13 | ||
|
|
3320125d8f | ||
|
|
a1f93b09f7 | ||
|
|
232acaeff2 | ||
|
|
d86104577b | ||
|
|
e9937fab76 | ||
|
|
75c2011845 | ||
|
|
5604b37c02 | ||
|
|
f604856eab | ||
|
|
b69e4350a1 | ||
|
|
95da706fed | ||
|
|
1bb67217e5 | ||
|
|
21f1b70daf | ||
|
|
430812abf5 | ||
|
|
8d5e28ef30 | ||
|
|
4180d81819 | ||
|
|
21eeb9ae02 | ||
|
|
edd2e9dd4b | ||
|
|
b25ac5d707 | ||
|
|
9d10cff873 | ||
|
|
bc438ce58a | ||
|
|
b99a68e55f | ||
|
|
7a819f4955 | ||
|
|
6b78b1ad8b | ||
|
|
488e28e874 | ||
|
|
d26dd5a8cf | ||
|
|
67f0fa26ae | ||
|
|
a3211f4458 | ||
|
|
7d4e5ff0bd | ||
|
|
f6c2008540 | ||
|
|
d74ca4940b | ||
|
|
739807a7d7 | ||
|
|
9fa26be13a | ||
|
|
f0555af284 | ||
|
|
b7fa9b09fe | ||
|
|
08f7a7a476 | ||
|
|
703196b013 | ||
|
|
284986e7b7 | ||
|
|
09ac1b866e | ||
|
|
4112cc42df | ||
|
|
c07e04ce84 | ||
|
|
19455b4d6c | ||
|
|
1186622e58 | ||
|
|
023e02da15 | ||
|
|
8dbf049a71 | ||
|
|
fe5150dba4 | ||
|
|
992d5a7a70 | ||
|
|
b702b6e14e | ||
|
|
557c76233a | ||
|
|
6ba9903a37 | ||
|
|
81bf9894e4 | ||
|
|
047d64983c | ||
|
|
1708b929b8 | ||
|
|
a377514287 | ||
|
|
c51d696181 | ||
|
|
1a9d328f94 | ||
|
|
3b1dc60f63 | ||
|
|
0c2ca1737e | ||
|
|
2f65a26dc7 | ||
|
|
37c765a072 | ||
|
|
29f1f860d8 | ||
|
|
414a416938 | ||
|
|
1a4180f678 | ||
|
|
bca086d776 | ||
|
|
f81086f40c | ||
|
|
962fb12410 | ||
|
|
10af787324 | ||
|
|
5f32bbbd81 | ||
|
|
fa78d4e783 | ||
|
|
05b03b2086 | ||
|
|
866b1d32b3 | ||
|
|
8a1f00debb | ||
|
|
3ec5755930 | ||
|
|
a0d173feb8 | ||
|
|
b0f678411c | ||
|
|
1369eaeef6 | ||
|
|
bf217345ae | ||
|
|
7bb567761c | ||
|
|
4b94ed3e55 | ||
|
|
75c0d22f87 | ||
|
|
9803e86158 | ||
|
|
0f7e1d5517 | ||
|
|
cf7a6ecc89 | ||
|
|
5c9dce0f8a | ||
|
|
041aba9a0b | ||
|
|
b13c44eb12 | ||
|
|
a47bf2e8fe | ||
|
|
4b8dd7b981 | ||
|
|
d2c870db92 | ||
|
|
8e17039586 | ||
|
|
1310e5dde9 |
@@ -34,7 +34,7 @@ BreakBeforeBinaryOperators: false
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: true
|
||||
ColumnLimit: 80
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
# clang-format
|
||||
e41150248a97e4bdc1cf21b54650c4bb7c63928e
|
||||
2e542e7b0d94451a933c88778461cc8d3d7e6417
|
||||
d816ef54abd8e8e979b9c795bdb657a8d18f5e95
|
||||
|
||||
20
.githooks/ensure_release_tag
Executable file
20
.githooks/ensure_release_tag
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Pushing a release branch requires an annotated tag at the released commit
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [[ $branch =~ master ]]; then
|
||||
# check if HEAD commit is tagged
|
||||
if ! git describe --exact-match HEAD; then
|
||||
echo "Commits to master must be tagged"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ $branch =~ release/* ]]; then
|
||||
IFS=/ read -r branch rel_ver <<< ${branch}
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
if [[ "${rel_ver}" != "${tag}" ]]; then
|
||||
echo "release/${rel_ver} branches must have annotated tag ${rel_ver}"
|
||||
echo "git tag -am\"${rel_ver}\" ${rel_ver}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
exec 1>&2
|
||||
|
||||
# format all relevant sources
|
||||
find src unittests -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 clang-format -i
|
||||
# paths to check and re-format
|
||||
sources="src unittests"
|
||||
formatter="clang-format-11 -i"
|
||||
|
||||
# check how many lines differ
|
||||
lines=$(git diff src unittests | wc -l)
|
||||
first=$(git diff $sources)
|
||||
find $sources -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 $formatter
|
||||
second=$(git diff $sources)
|
||||
changes=$(diff <(echo "$first") <(echo "$second") | wc -l | sed -e 's/^[[:space:]]*//')
|
||||
|
||||
# check if there is any updated files
|
||||
if [ "$lines" != "0" ]; then
|
||||
if [ "$changes" != "0" ]; then
|
||||
cat <<\EOF
|
||||
|
||||
WARNING
|
||||
WARNING
|
||||
-----------------------------------------------------------------------------
|
||||
Automatically re-formatted code with `clang-format` - commit was aborted.
|
||||
Please manually add any updated files and commit again.
|
||||
@@ -21,3 +23,5 @@ if [ "$lines" != "0" ]; then
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
.githooks/ensure_release_tag
|
||||
|
||||
21
.github/actions/sign/action.yml
vendored
21
.github/actions/sign/action.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: 'Sign packages'
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- name: Sign
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex -o pipefail
|
||||
echo "$GPG_KEY_B64"| base64 -d | gpg --batch --no-tty --allow-secret-key-import --import -
|
||||
unset GPG_KEY_B64
|
||||
export GPG_PASSPHRASE=$(echo $GPG_KEY_PASS_B64 | base64 -di)
|
||||
unset GPG_KEY_PASS_B64
|
||||
export GPG_KEYID=$(gpg --with-colon --list-secret-keys | head -n1 | cut -d : -f 5)
|
||||
for PKG in $(ls *.deb); do
|
||||
dpkg-sig \
|
||||
-g "--no-tty --digest-algo 'sha512' --passphrase '${GPG_PASSPHRASE}' --pinentry-mode=loopback" \
|
||||
-k "${GPG_KEYID}" \
|
||||
--sign builder \
|
||||
$PKG
|
||||
done
|
||||
176
.github/workflows/build.yml
vendored
176
.github/workflows/build.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Build Clio
|
||||
on:
|
||||
push:
|
||||
branches: [master, release, develop, develop-next]
|
||||
branches: [master, release/*, develop, develop-next]
|
||||
pull_request:
|
||||
branches: [master, release, develop, develop-next]
|
||||
branches: [master, release/*, develop, develop-next]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
build_clio:
|
||||
name: Build Clio
|
||||
runs-on: [self-hosted, Linux]
|
||||
runs-on: [self-hosted, heavy]
|
||||
needs: lint
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -29,19 +29,21 @@ jobs:
|
||||
- suffix: rpm
|
||||
image: rippleci/clio-rpm-builder:2022-09-17
|
||||
script: rpm
|
||||
|
||||
container:
|
||||
image: ${{ matrix.type.image }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: clio
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Clone Clio packaging repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: clio-packages
|
||||
repository: XRPLF/clio-packages
|
||||
ref: main
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
@@ -54,7 +56,6 @@ jobs:
|
||||
cmake --build clio-packages/build --parallel $(nproc)
|
||||
cp ./clio-packages/build/clio-prefix/src/clio-build/clio_tests .
|
||||
mv ./clio-packages/build/*.${{ matrix.type.suffix }} .
|
||||
|
||||
- name: Artifact packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -67,59 +68,57 @@ jobs:
|
||||
name: clio_tests-${{ matrix.type.suffix }}
|
||||
path: ${{ github.workspace }}/clio_tests
|
||||
|
||||
sign:
|
||||
name: Sign packages
|
||||
needs: build_clio
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/release' || github.ref == 'refs/heads/develop'
|
||||
env:
|
||||
GPG_KEY_B64: ${{ secrets.GPG_KEY_B64 }}
|
||||
GPG_KEY_PASS_B64: ${{ secrets.GPG_KEY_PASS_B64 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- suffix: deb
|
||||
image: ubuntu:20.04
|
||||
script: dpkg
|
||||
# - suffix: rpm
|
||||
# image: centos:7
|
||||
# script: rpm
|
||||
container:
|
||||
image: ${{ matrix.type.image }}
|
||||
build_dev:
|
||||
name: Build on Mac/Clang14 and run tests
|
||||
needs: lint
|
||||
continue-on-error: false
|
||||
runs-on: [self-hosted, macOS]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dpkg-sig
|
||||
run: |
|
||||
apt-get update && apt-get install -y dpkg-sig gnupg
|
||||
- name: Get package artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: clio_${{ matrix.type.suffix }}_packages
|
||||
path: clio
|
||||
|
||||
- name: find packages
|
||||
run: find . -name "*.${{ matrix.type.suffix }}"
|
||||
|
||||
- name: Sign packages
|
||||
uses: ./.github/actions/sign
|
||||
|
||||
|
||||
- name: Verify the signature
|
||||
run: |
|
||||
set -e
|
||||
for PKG in $(ls *.deb); do
|
||||
gpg --verify "${PKG}"
|
||||
done
|
||||
|
||||
- name: Get short SHA
|
||||
id: shortsha
|
||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
||||
|
||||
- name: Artifact signed packages
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Check Boost cache
|
||||
id: boost
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
name: signed-clio-deb-packages-${{ steps.shortsha.outputs.sha8 }}
|
||||
path: ${{ github.workspace }}/*.deb
|
||||
path: boost_1_77_0
|
||||
key: ${{ runner.os }}-boost
|
||||
|
||||
- name: Build Boost
|
||||
if: ${{ steps.boost.outputs.cache-hit != 'true' }}
|
||||
run: |
|
||||
rm -rf boost_1_77_0.tar.gz boost_1_77_0 # cleanup if needed first
|
||||
curl -s -fOJL "https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.gz"
|
||||
tar zxf boost_1_77_0.tar.gz
|
||||
cd boost_1_77_0
|
||||
./bootstrap.sh
|
||||
./b2 define=BOOST_ASIO_HAS_STD_INVOKE_RESULT cxxflags="-std=c++20"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install llvm@14 pkg-config protobuf openssl ninja cassandra-cpp-driver bison cmake
|
||||
|
||||
- name: Setup environment for llvm-14
|
||||
run: |
|
||||
export PATH="/usr/local/opt/llvm@14/bin:$PATH"
|
||||
export LDFLAGS="-L/usr/local/opt/llvm@14/lib -L/usr/local/opt/llvm@14/lib/c++ -Wl,-rpath,/usr/local/opt/llvm@14/lib/c++"
|
||||
export CPPFLAGS="-I/usr/local/opt/llvm@14/include"
|
||||
|
||||
- name: Build clio
|
||||
run: |
|
||||
export BOOST_ROOT=$(pwd)/boost_1_77_0
|
||||
cd clio
|
||||
cmake -B build -DCMAKE_C_COMPILER='/usr/local/opt/llvm@14/bin/clang' -DCMAKE_CXX_COMPILER='/usr/local/opt/llvm@14/bin/clang++'
|
||||
if ! cmake --build build -j; then
|
||||
echo '# 🔥🔥 MacOS AppleClang build failed!💥' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
- name: Run Test
|
||||
run: |
|
||||
cd clio/build
|
||||
./clio_tests --gtest_filter="-BackendTest*:BackendCassandraBaseTest*:BackendCassandraTest*"
|
||||
|
||||
test_clio:
|
||||
name: Test Clio
|
||||
@@ -140,3 +139,74 @@ jobs:
|
||||
- name: Run tests
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/test
|
||||
|
||||
code_coverage:
|
||||
name: Build on Linux and code coverage
|
||||
needs: lint
|
||||
continue-on-error: false
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: clio
|
||||
|
||||
- name: Check Boost cache
|
||||
id: boost
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: boost
|
||||
key: ${{ runner.os }}-boost
|
||||
|
||||
- name: Build boost
|
||||
if: steps.boost.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -s -OJL "https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.gz"
|
||||
tar zxf boost_1_77_0.tar.gz
|
||||
mv boost_1_77_0 boost
|
||||
cd boost
|
||||
./bootstrap.sh
|
||||
./b2
|
||||
- name: install deps
|
||||
run: |
|
||||
sudo apt-get -y install git pkg-config protobuf-compiler libprotobuf-dev libssl-dev wget build-essential doxygen bison flex autoconf clang-format gcovr
|
||||
- name: Build clio
|
||||
run: |
|
||||
export BOOST_ROOT=$(pwd)/boost
|
||||
cd clio
|
||||
cmake -B build -DCODE_COVERAGE=on -DTEST_PARAMETER='--gtest_filter="-BackendTest*:BackendCassandraBaseTest*:BackendCassandraTest*"'
|
||||
if ! cmake --build build -j$(nproc); then
|
||||
echo '# 🔥Ubuntu build🔥 failed!💥' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
cd build
|
||||
make clio_tests-ccov
|
||||
- name: Code Coverage Summary Report
|
||||
uses: irongut/CodeCoverageSummary@v1.2.0
|
||||
with:
|
||||
filename: clio/build/clio_tests-gcc-cov/out.xml
|
||||
badge: true
|
||||
output: both
|
||||
format: markdown
|
||||
|
||||
- name: Save PR number and ccov report
|
||||
run: |
|
||||
mkdir -p ./UnitTestCoverage
|
||||
echo ${{ github.event.number }} > ./UnitTestCoverage/NR
|
||||
cp clio/build/clio_tests-gcc-cov/report.html ./UnitTestCoverage/report.html
|
||||
cp code-coverage-results.md ./UnitTestCoverage/out.md
|
||||
cat code-coverage-results.md > $GITHUB_STEP_SUMMARY
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: clio/build/clio_tests-gcc-cov/out.xml
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: UnitTestCoverage
|
||||
path: UnitTestCoverage/
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code_coverage_report
|
||||
path: clio/build/clio_tests-gcc-cov/out.xml
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*clio*.log
|
||||
build/
|
||||
build*/
|
||||
.vscode
|
||||
.python-version
|
||||
config.json
|
||||
src/main/impl/Build.cpp
|
||||
|
||||
39
CMake/Build.cpp.in
Normal file
39
CMake/Build.cpp.in
Normal file
@@ -0,0 +1,39 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <main/Build.h>
|
||||
|
||||
namespace Build {
|
||||
static constexpr char versionString[] = "@VERSION@";
|
||||
|
||||
std::string const&
|
||||
getClioVersionString()
|
||||
{
|
||||
static std::string const value = versionString;
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
getClioFullVersionString()
|
||||
{
|
||||
static std::string const value = "clio-" + getClioVersionString();
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace Build
|
||||
@@ -1,15 +1,33 @@
|
||||
#[===================================================================[
|
||||
read version from source
|
||||
write version to source
|
||||
#]===================================================================]
|
||||
|
||||
file (STRINGS src/main/impl/Build.cpp BUILD_INFO)
|
||||
foreach (line_ ${BUILD_INFO})
|
||||
if (line_ MATCHES "versionString[ ]*=[ ]*\"(.+)\"")
|
||||
set (clio_version ${CMAKE_MATCH_1})
|
||||
endif ()
|
||||
endforeach ()
|
||||
if (clio_version)
|
||||
message (STATUS "clio version: ${clio_version}")
|
||||
else ()
|
||||
message (FATAL_ERROR "unable to determine clio version")
|
||||
endif ()
|
||||
find_package(Git REQUIRED)
|
||||
|
||||
set(GIT_COMMAND rev-parse --short HEAD)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE REV OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
set(GIT_COMMAND branch --show-current)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
if(BRANCH STREQUAL "")
|
||||
set(BRANCH "dev")
|
||||
endif()
|
||||
|
||||
if(NOT (BRANCH MATCHES master OR BRANCH MATCHES release/*)) # for develop and any other branch name YYYYMMDDHMS-<branch>-<git-ref>
|
||||
execute_process(COMMAND date +%Y%m%d%H%M%S OUTPUT_VARIABLE DATE OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(VERSION "${DATE}-${BRANCH}-${REV}")
|
||||
else()
|
||||
set(GIT_COMMAND describe --tags)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE TAG_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(VERSION "${TAG_VERSION}-${REV}")
|
||||
endif()
|
||||
|
||||
if(CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
set(VERSION "${VERSION}+DEBUG")
|
||||
endif()
|
||||
|
||||
message(STATUS "Build version: ${VERSION}")
|
||||
set(clio_version "${VERSION}")
|
||||
|
||||
configure_file(CMake/Build.cpp.in ${CMAKE_SOURCE_DIR}/src/main/impl/Build.cpp)
|
||||
|
||||
126
CMake/coverage.cmake
Normal file
126
CMake/coverage.cmake
Normal file
@@ -0,0 +1,126 @@
|
||||
# call add_converage(module_name) to add coverage targets for the given module
|
||||
function(add_converage module)
|
||||
if("${CMAKE_C_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang"
|
||||
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
|
||||
message("[Coverage] Building with llvm Code Coverage Tools")
|
||||
# Using llvm gcov ; llvm install by xcode
|
||||
set(LLVM_COV_PATH /Library/Developer/CommandLineTools/usr/bin)
|
||||
if(NOT EXISTS ${LLVM_COV_PATH}/llvm-cov)
|
||||
message(FATAL_ERROR "llvm-cov not found! Aborting.")
|
||||
endif()
|
||||
|
||||
# set Flags
|
||||
target_compile_options(${module} PRIVATE -fprofile-instr-generate
|
||||
-fcoverage-mapping)
|
||||
target_link_options(${module} PUBLIC -fprofile-instr-generate
|
||||
-fcoverage-mapping)
|
||||
|
||||
target_compile_options(clio PRIVATE -fprofile-instr-generate
|
||||
-fcoverage-mapping)
|
||||
target_link_options(clio PUBLIC -fprofile-instr-generate
|
||||
-fcoverage-mapping)
|
||||
|
||||
# llvm-cov
|
||||
add_custom_target(
|
||||
${module}-ccov-preprocessing
|
||||
COMMAND LLVM_PROFILE_FILE=${module}.profraw $<TARGET_FILE:${module}>
|
||||
COMMAND ${LLVM_COV_PATH}/llvm-profdata merge -sparse ${module}.profraw -o
|
||||
${module}.profdata
|
||||
DEPENDS ${module})
|
||||
|
||||
add_custom_target(
|
||||
${module}-ccov-show
|
||||
COMMAND ${LLVM_COV_PATH}/llvm-cov show $<TARGET_FILE:${module}>
|
||||
-instr-profile=${module}.profdata -show-line-counts-or-regions
|
||||
DEPENDS ${module}-ccov-preprocessing)
|
||||
|
||||
# add summary for CI parse
|
||||
add_custom_target(
|
||||
${module}-ccov-report
|
||||
COMMAND
|
||||
${LLVM_COV_PATH}/llvm-cov report $<TARGET_FILE:${module}>
|
||||
-instr-profile=${module}.profdata
|
||||
-ignore-filename-regex=".*_makefiles|.*unittests|.*_deps"
|
||||
-show-region-summary=false
|
||||
DEPENDS ${module}-ccov-preprocessing)
|
||||
|
||||
# exclude libs and unittests self
|
||||
add_custom_target(
|
||||
${module}-ccov
|
||||
COMMAND
|
||||
${LLVM_COV_PATH}/llvm-cov show $<TARGET_FILE:${module}>
|
||||
-instr-profile=${module}.profdata -show-line-counts-or-regions
|
||||
-output-dir=${module}-llvm-cov -format="html"
|
||||
-ignore-filename-regex=".*_makefiles|.*unittests|.*_deps" > /dev/null 2>&1
|
||||
DEPENDS ${module}-ccov-preprocessing)
|
||||
|
||||
add_custom_command(
|
||||
TARGET ${module}-ccov
|
||||
POST_BUILD
|
||||
COMMENT
|
||||
"Open ${module}-llvm-cov/index.html in your browser to view the coverage report."
|
||||
)
|
||||
elseif("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR "${CMAKE_CXX_COMPILER_ID}"
|
||||
MATCHES "GNU")
|
||||
message("[Coverage] Building with Gcc Code Coverage Tools")
|
||||
|
||||
find_program(GCOV_PATH gcov)
|
||||
if(NOT GCOV_PATH)
|
||||
message(FATAL_ERROR "gcov not found! Aborting...")
|
||||
endif() # NOT GCOV_PATH
|
||||
find_program(GCOVR_PATH gcovr)
|
||||
if(NOT GCOVR_PATH)
|
||||
message(FATAL_ERROR "gcovr not found! Aborting...")
|
||||
endif() # NOT GCOVR_PATH
|
||||
|
||||
set(COV_OUTPUT_PATH ${module}-gcc-cov)
|
||||
target_compile_options(${module} PRIVATE -fprofile-arcs -ftest-coverage
|
||||
-fPIC)
|
||||
target_link_libraries(${module} PRIVATE gcov)
|
||||
|
||||
target_compile_options(clio PRIVATE -fprofile-arcs -ftest-coverage
|
||||
-fPIC)
|
||||
target_link_libraries(clio PRIVATE gcov)
|
||||
# this target is used for CI as well generate the summary out.xml will send
|
||||
# to github action to generate markdown, we can paste it to comments or
|
||||
# readme
|
||||
add_custom_target(
|
||||
${module}-ccov
|
||||
COMMAND ${module} ${TEST_PARAMETER}
|
||||
COMMAND rm -rf ${COV_OUTPUT_PATH}
|
||||
COMMAND mkdir ${COV_OUTPUT_PATH}
|
||||
COMMAND
|
||||
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR} -x
|
||||
${COV_OUTPUT_PATH}/out.xml --exclude='${CMAKE_SOURCE_DIR}/unittests/'
|
||||
--exclude='${PROJECT_BINARY_DIR}/'
|
||||
COMMAND
|
||||
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR}
|
||||
--html ${COV_OUTPUT_PATH}/report.html
|
||||
--exclude='${CMAKE_SOURCE_DIR}/unittests/'
|
||||
--exclude='${PROJECT_BINARY_DIR}/'
|
||||
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
|
||||
COMMENT "Running gcovr to produce Cobertura code coverage report.")
|
||||
|
||||
# generate the detail report
|
||||
add_custom_target(
|
||||
${module}-ccov-report
|
||||
COMMAND ${module} ${TEST_PARAMETER}
|
||||
COMMAND rm -rf ${COV_OUTPUT_PATH}
|
||||
COMMAND mkdir ${COV_OUTPUT_PATH}
|
||||
COMMAND
|
||||
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR}
|
||||
--html-details ${COV_OUTPUT_PATH}/index.html
|
||||
--exclude='${CMAKE_SOURCE_DIR}/unittests/'
|
||||
--exclude='${PROJECT_BINARY_DIR}/'
|
||||
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
|
||||
COMMENT "Running gcovr to produce Cobertura code coverage report.")
|
||||
add_custom_command(
|
||||
TARGET ${module}-ccov-report
|
||||
POST_BUILD
|
||||
COMMENT
|
||||
"Open ${COV_OUTPUT_PATH}/index.html in your browser to view the coverage report."
|
||||
)
|
||||
else()
|
||||
message(FATAL_ERROR "Complier not support yet")
|
||||
endif()
|
||||
endfunction()
|
||||
11
CMake/deps/SourceLocation.cmake
Normal file
11
CMake/deps/SourceLocation.cmake
Normal file
@@ -0,0 +1,11 @@
|
||||
include(CheckIncludeFileCXX)
|
||||
|
||||
check_include_file_cxx("source_location" SOURCE_LOCATION_AVAILABLE)
|
||||
if(SOURCE_LOCATION_AVAILABLE)
|
||||
target_compile_definitions(clio PUBLIC "HAS_SOURCE_LOCATION")
|
||||
endif()
|
||||
|
||||
check_include_file_cxx("experimental/source_location" EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
|
||||
if(EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
|
||||
target_compile_definitions(clio PUBLIC "HAS_EXPERIMENTAL_SOURCE_LOCATION")
|
||||
endif()
|
||||
@@ -144,8 +144,10 @@ if(NOT cassandra)
|
||||
else()
|
||||
message("Found system installed cassandra cpp driver")
|
||||
message(${cassandra})
|
||||
|
||||
find_path(cassandra_includes NAMES cassandra.h REQUIRED)
|
||||
message(${cassandra_includes})
|
||||
get_filename_component(CASSANDRA_HEADER ${cassandra_includes}/cassandra.h REALPATH)
|
||||
get_filename_component(CASSANDRA_HEADER_DIR ${CASSANDRA_HEADER} DIRECTORY)
|
||||
target_link_libraries (clio PUBLIC ${cassandra})
|
||||
target_include_directories(clio INTERFACE ${cassandra_includes})
|
||||
target_include_directories(clio PUBLIC ${CASSANDRA_HEADER_DIR})
|
||||
endif()
|
||||
|
||||
@@ -10,7 +10,8 @@ if(NOT googletest_POPULATED)
|
||||
add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
target_link_libraries(clio_tests PUBLIC clio gtest_main)
|
||||
target_link_libraries(clio_tests PUBLIC clio gmock_main)
|
||||
target_include_directories(clio_tests PRIVATE unittests)
|
||||
|
||||
enable_testing()
|
||||
|
||||
|
||||
14
CMake/deps/libfmt.cmake
Normal file
14
CMake/deps/libfmt.cmake
Normal file
@@ -0,0 +1,14 @@
|
||||
FetchContent_Declare(
|
||||
libfmt
|
||||
URL https://github.com/fmtlib/fmt/releases/download/9.1.0/fmt-9.1.0.zip
|
||||
)
|
||||
|
||||
FetchContent_GetProperties(libfmt)
|
||||
|
||||
if(NOT libfmt_POPULATED)
|
||||
FetchContent_Populate(libfmt)
|
||||
add_subdirectory(${libfmt_SOURCE_DIR} ${libfmt_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
target_link_libraries(clio PUBLIC fmt)
|
||||
|
||||
@@ -11,6 +11,7 @@ ExecStart=@CLIO_INSTALL_DIR@/bin/clio_server @CLIO_INSTALL_DIR@/etc/config.json
|
||||
Restart=on-failure
|
||||
User=clio
|
||||
Group=clio
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-narrowing -Wall -Werror -Wno-dangling-else")
|
||||
target_compile_options(clio
|
||||
PUBLIC -Wall
|
||||
-Werror
|
||||
-Wno-narrowing
|
||||
-Wno-deprecated-declarations
|
||||
-Wno-dangling-else)
|
||||
|
||||
146
CMakeLists.txt
146
CMakeLists.txt
@@ -14,25 +14,13 @@ if(VERBOSE)
|
||||
set(FETCHCONTENT_QUIET FALSE CACHE STRING "Verbose FetchContent()")
|
||||
endif()
|
||||
|
||||
if(NOT GIT_COMMIT_HASH)
|
||||
if(VERBOSE)
|
||||
message("GIT_COMMIT_HASH not provided...looking for git")
|
||||
endif()
|
||||
find_package(Git)
|
||||
if(Git_FOUND)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE gch)
|
||||
if(gch)
|
||||
set(GIT_COMMIT_HASH "${gch}")
|
||||
message(STATUS "Git commit: ${GIT_COMMIT_HASH}")
|
||||
add_definitions(-DCLIO_GIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
endif()
|
||||
endif()
|
||||
endif() #git
|
||||
|
||||
if(PACKAGING)
|
||||
add_definitions(-DPKG=1)
|
||||
endif()
|
||||
|
||||
#c++20 removed std::result_of but boost 1.75 is still using it.
|
||||
add_definitions(-DBOOST_ASIO_HAS_STD_INVOKE_RESULT=1)
|
||||
|
||||
add_library(clio)
|
||||
target_compile_features(clio PUBLIC cxx_std_20)
|
||||
@@ -43,8 +31,10 @@ include(ExternalProject)
|
||||
include(CMake/settings.cmake)
|
||||
include(CMake/ClioVersion.cmake)
|
||||
include(CMake/deps/rippled.cmake)
|
||||
include(CMake/deps/libfmt.cmake)
|
||||
include(CMake/deps/Boost.cmake)
|
||||
include(CMake/deps/cassandra.cmake)
|
||||
include(CMake/deps/SourceLocation.cmake)
|
||||
|
||||
target_sources(clio PRIVATE
|
||||
## Main
|
||||
@@ -53,6 +43,15 @@ target_sources(clio PRIVATE
|
||||
src/backend/BackendInterface.cpp
|
||||
src/backend/CassandraBackend.cpp
|
||||
src/backend/SimpleCache.cpp
|
||||
## NextGen Backend
|
||||
src/backend/cassandra/impl/Future.cpp
|
||||
src/backend/cassandra/impl/Cluster.cpp
|
||||
src/backend/cassandra/impl/Batch.cpp
|
||||
src/backend/cassandra/impl/Result.cpp
|
||||
src/backend/cassandra/impl/Tuple.cpp
|
||||
src/backend/cassandra/impl/SslContext.cpp
|
||||
src/backend/cassandra/Handle.cpp
|
||||
src/backend/cassandra/SettingsProvider.cpp
|
||||
## ETL
|
||||
src/etl/ETLSource.cpp
|
||||
src/etl/ProbingETLSource.cpp
|
||||
@@ -62,57 +61,112 @@ target_sources(clio PRIVATE
|
||||
src/subscriptions/SubscriptionManager.cpp
|
||||
## RPC
|
||||
src/rpc/Errors.cpp
|
||||
src/rpc/RPC.cpp
|
||||
src/rpc/Factories.cpp
|
||||
src/rpc/RPCHelpers.cpp
|
||||
src/rpc/Counters.cpp
|
||||
src/rpc/WorkQueue.cpp
|
||||
## RPC Methods
|
||||
# Account
|
||||
src/rpc/common/Specs.cpp
|
||||
src/rpc/common/Validators.cpp
|
||||
# RPC impl
|
||||
src/rpc/common/impl/HandlerProvider.cpp
|
||||
## RPC handler
|
||||
src/rpc/handlers/AccountChannels.cpp
|
||||
src/rpc/handlers/AccountCurrencies.cpp
|
||||
src/rpc/handlers/AccountInfo.cpp
|
||||
src/rpc/handlers/AccountLines.cpp
|
||||
src/rpc/handlers/AccountOffers.cpp
|
||||
src/rpc/handlers/AccountNFTs.cpp
|
||||
src/rpc/handlers/AccountObjects.cpp
|
||||
src/rpc/handlers/AccountOffers.cpp
|
||||
src/rpc/handlers/AccountTx.cpp
|
||||
src/rpc/handlers/BookChanges.cpp
|
||||
src/rpc/handlers/BookOffers.cpp
|
||||
src/rpc/handlers/GatewayBalances.cpp
|
||||
src/rpc/handlers/NoRippleCheck.cpp
|
||||
# NFT
|
||||
src/rpc/handlers/NFTHistory.cpp
|
||||
src/rpc/handlers/NFTInfo.cpp
|
||||
src/rpc/handlers/NFTOffers.cpp
|
||||
# Ledger
|
||||
src/rpc/handlers/Ledger.cpp
|
||||
src/rpc/handlers/LedgerData.cpp
|
||||
src/rpc/handlers/LedgerEntry.cpp
|
||||
src/rpc/handlers/LedgerRange.cpp
|
||||
# Transaction
|
||||
src/rpc/handlers/Tx.cpp
|
||||
src/rpc/handlers/TransactionEntry.cpp
|
||||
src/rpc/handlers/AccountTx.cpp
|
||||
# Dex
|
||||
src/rpc/handlers/BookChanges.cpp
|
||||
src/rpc/handlers/BookOffers.cpp
|
||||
# Payment Channel
|
||||
src/rpc/handlers/ChannelAuthorize.cpp
|
||||
src/rpc/handlers/ChannelVerify.cpp
|
||||
# Subscribe
|
||||
src/rpc/handlers/Subscribe.cpp
|
||||
# Server
|
||||
src/rpc/handlers/ServerInfo.cpp
|
||||
# Utility
|
||||
src/rpc/handlers/NFTBuyOffers.cpp
|
||||
src/rpc/handlers/NFTHistory.cpp
|
||||
src/rpc/handlers/NFTInfo.cpp
|
||||
src/rpc/handlers/NFTOffersCommon.cpp
|
||||
src/rpc/handlers/NFTSellOffers.cpp
|
||||
src/rpc/handlers/NoRippleCheck.cpp
|
||||
src/rpc/handlers/Random.cpp
|
||||
src/util/Taggable.cpp
|
||||
src/config/Config.cpp)
|
||||
src/rpc/handlers/TransactionEntry.cpp
|
||||
src/rpc/handlers/Tx.cpp
|
||||
## Util
|
||||
src/config/Config.cpp
|
||||
src/log/Logger.cpp
|
||||
src/util/Taggable.cpp)
|
||||
|
||||
add_executable(clio_server src/main/main.cpp)
|
||||
target_link_libraries(clio_server PUBLIC clio)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_executable(clio_tests
|
||||
unittests/RPCErrors.cpp
|
||||
unittests/config.cpp
|
||||
unittests/main.cpp)
|
||||
set(TEST_TARGET clio_tests)
|
||||
add_executable(${TEST_TARGET}
|
||||
unittests/Playground.cpp
|
||||
unittests/Backend.cpp
|
||||
unittests/Logger.cpp
|
||||
unittests/Config.cpp
|
||||
unittests/ProfilerTest.cpp
|
||||
unittests/DOSGuard.cpp
|
||||
unittests/SubscriptionTest.cpp
|
||||
unittests/SubscriptionManagerTest.cpp
|
||||
unittests/util/TestObject.cpp
|
||||
# RPC
|
||||
unittests/rpc/ErrorTests.cpp
|
||||
unittests/rpc/BaseTests.cpp
|
||||
unittests/rpc/RPCHelpersTest.cpp
|
||||
unittests/rpc/CountersTest.cpp
|
||||
unittests/rpc/AdminVerificationTest.cpp
|
||||
## RPC handlers
|
||||
unittests/rpc/handlers/DefaultProcessorTests.cpp
|
||||
unittests/rpc/handlers/TestHandlerTests.cpp
|
||||
unittests/rpc/handlers/AccountCurrenciesTest.cpp
|
||||
unittests/rpc/handlers/AccountLinesTest.cpp
|
||||
unittests/rpc/handlers/AccountTxTest.cpp
|
||||
unittests/rpc/handlers/AccountOffersTest.cpp
|
||||
unittests/rpc/handlers/AccountInfoTest.cpp
|
||||
unittests/rpc/handlers/AccountChannelsTest.cpp
|
||||
unittests/rpc/handlers/AccountNFTsTest.cpp
|
||||
unittests/rpc/handlers/BookOffersTest.cpp
|
||||
unittests/rpc/handlers/GatewayBalancesTest.cpp
|
||||
unittests/rpc/handlers/TxTest.cpp
|
||||
unittests/rpc/handlers/TransactionEntryTest.cpp
|
||||
unittests/rpc/handlers/LedgerEntryTest.cpp
|
||||
unittests/rpc/handlers/LedgerRangeTest.cpp
|
||||
unittests/rpc/handlers/NoRippleCheckTest.cpp
|
||||
unittests/rpc/handlers/ServerInfoTest.cpp
|
||||
unittests/rpc/handlers/PingTest.cpp
|
||||
unittests/rpc/handlers/RandomTest.cpp
|
||||
unittests/rpc/handlers/NFTInfoTest.cpp
|
||||
unittests/rpc/handlers/NFTBuyOffersTest.cpp
|
||||
unittests/rpc/handlers/NFTSellOffersTest.cpp
|
||||
unittests/rpc/handlers/NFTHistoryTest.cpp
|
||||
unittests/rpc/handlers/SubscribeTest.cpp
|
||||
unittests/rpc/handlers/UnsubscribeTest.cpp
|
||||
unittests/rpc/handlers/LedgerDataTest.cpp
|
||||
unittests/rpc/handlers/AccountObjectsTest.cpp
|
||||
unittests/rpc/handlers/BookChangesTest.cpp
|
||||
unittests/rpc/handlers/LedgerTest.cpp
|
||||
# Backend
|
||||
unittests/backend/cassandra/BaseTests.cpp
|
||||
unittests/backend/cassandra/BackendTests.cpp
|
||||
unittests/backend/cassandra/RetryPolicyTests.cpp
|
||||
unittests/backend/cassandra/SettingsProviderTests.cpp
|
||||
unittests/backend/cassandra/ExecutionStrategyTests.cpp
|
||||
unittests/backend/cassandra/AsyncExecutorTests.cpp)
|
||||
include(CMake/deps/gtest.cmake)
|
||||
|
||||
# test for dwarf5 bug on ci
|
||||
target_compile_options(clio PUBLIC -gdwarf-4)
|
||||
|
||||
# if CODE_COVERAGE enable, add clio_test-ccov
|
||||
if(CODE_COVERAGE)
|
||||
include(CMake/coverage.cmake)
|
||||
add_converage(${TEST_TARGET})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(CMake/install/install.cmake)
|
||||
|
||||
@@ -3,18 +3,18 @@ Thank you for your interest in contributing to the `clio` project 🙏
|
||||
|
||||
To contribute, please:
|
||||
1. Fork the repository under your own user.
|
||||
2. Create a new branch on which to write your changes.
|
||||
2. Create a new branch on which to commit/push your changes.
|
||||
3. Write and test your code.
|
||||
4. Ensure that your code compiles with the provided build engine and update the provided build engine as part of your PR where needed and where appropriate.
|
||||
5. Where applicable, write test cases for your code and include those in `unittests`.
|
||||
6. Ensure your code passes automated checks (e.g. clang-format)
|
||||
7. Squash your commits (i.e. rebase) into as few commits as is reasonable to describe your changes at a high level (typically a single commit for a small change.). See below for more details.
|
||||
7. Squash your commits (i.e. rebase) into as few commits as is reasonable to describe your changes at a high level (typically a single commit for a small change). See below for more details.
|
||||
8. Open a PR to the main repository onto the _develop_ branch, and follow the provided template.
|
||||
|
||||
> **Note:** Please make sure you read the [Style guide](#style-guide).
|
||||
> **Note:** Please read the [Style guide](#style-guide).
|
||||
|
||||
## Install git hooks
|
||||
Please make sure to run the following command in order to use git hooks that are helpful for `clio` development.
|
||||
Please run the following command in order to use git hooks that are helpful for `clio` development.
|
||||
|
||||
``` bash
|
||||
git config --local core.hooksPath .githooks
|
||||
@@ -22,10 +22,10 @@ git config --local core.hooksPath .githooks
|
||||
|
||||
## Git commands
|
||||
This sections offers a detailed look at the git commands you will need to use to get your PR submitted.
|
||||
Please note that there are more than one way to do this and these commands are only provided for your convenience.
|
||||
Please note that there are more than one way to do this and these commands are provided for your convenience.
|
||||
At this point it's assumed that you have already finished working on your feature/bug.
|
||||
|
||||
> **Important:** Before you issue any of the commands below, please hit the `Sync fork` button and make sure your fork's `develop` branch is up to date with the main `clio` repository.
|
||||
> **Important:** Before you issue any of the commands below, please hit the `Sync fork` button and make sure your fork's `develop` branch is up-to-date with the main `clio` repository.
|
||||
|
||||
``` bash
|
||||
# Create a backup of your branch
|
||||
@@ -37,16 +37,16 @@ git pull origin develop
|
||||
git checkout <your feature branch>
|
||||
git rebase -i develop
|
||||
```
|
||||
For each commit in the list other than the first one please select `s` to squash.
|
||||
After this is done you will have the opportunity to write a message for the squashed commit.
|
||||
For each commit in the list other than the first one, enter `s` to squash.
|
||||
After this is done, you will have the opportunity to write a message for the squashed commit.
|
||||
|
||||
> **Hint:** Please use **imperative mood** commit message capitalizing the first word of the subject.
|
||||
> **Hint:** Please use **imperative mood** in the commit message, and capitalize the first word.
|
||||
|
||||
``` bash
|
||||
# You should now have a single commit on top of a commit in `develop`
|
||||
git log
|
||||
```
|
||||
> **Todo:** In case there are merge conflicts, please resolve them now
|
||||
> **Note:** If there are merge conflicts, please resolve them now.
|
||||
|
||||
``` bash
|
||||
# Use the same commit message as you did above
|
||||
@@ -54,16 +54,16 @@ git commit -m 'Your message'
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
> **Important:** If you have no GPG keys setup please follow [this tutorial](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
|
||||
> **Important:** If you have no GPG keys set up, please follow [this tutorial](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
|
||||
|
||||
``` bash
|
||||
# Sign the commit with your GPG key and finally push your changes to the repo
|
||||
# Sign the commit with your GPG key, and push your changes
|
||||
git commit --amend -S
|
||||
git push --force
|
||||
```
|
||||
|
||||
## Fixing issues found during code review
|
||||
While your code is in review it's possible that some changes will be requested by the reviewer.
|
||||
While your code is in review, it's possible that some changes will be requested by reviewer(s).
|
||||
This section describes the process of adding your fixes.
|
||||
|
||||
We assume that you already made the required changes on your feature branch.
|
||||
@@ -72,25 +72,26 @@ We assume that you already made the required changes on your feature branch.
|
||||
# Add the changed code
|
||||
git add <paths to add>
|
||||
|
||||
# Add a folded commit message (so you can squash them later)
|
||||
# Add a [FOLD] commit message (so you remember to squash it later)
|
||||
# while also signing it with your GPG key
|
||||
git commit -S -m "[FOLD] Your commit message"
|
||||
|
||||
# And finally push your changes
|
||||
git push
|
||||
```
|
||||
## After code review
|
||||
Last but not least, when your PR is approved you still have to `Squash and merge` your code.
|
||||
Luckily there is a button for that towards the bottom of the PR's page on github.
|
||||
|
||||
> **Important:** Please leave the automatically generated link to PR in the subject line **and** in the description field please add `"Fixes #ISSUE_ID"` (replacing `ISSUE_ID` with yours).
|
||||
## After code review
|
||||
When your PR is approved and ready to merge, use `Squash and merge`.
|
||||
The button for that is near the bottom of the PR's page on GitHub.
|
||||
|
||||
> **Important:** Please leave the automatically-generated mention/link to the PR in the subject line **and** in the description field add `"Fix #ISSUE_ID"` (replacing `ISSUE_ID` with yours) if the PR fixes an issue.
|
||||
> **Note:** See [issues](https://github.com/XRPLF/clio/issues) to find the `ISSUE_ID` for the feature/bug you were working on.
|
||||
|
||||
# Style guide
|
||||
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent rather than a set of _thou shalt not_ commandments.
|
||||
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent.
|
||||
|
||||
## Formatting
|
||||
All code must conform to `clang-format` version 10, unless the result would be unreasonably difficult to read or maintain.
|
||||
Code must conform to `clang-format` version 10, unless the result would be unreasonably difficult to read or maintain.
|
||||
To change your code to conform use `clang-format -i <your changed files>`.
|
||||
|
||||
## Avoid
|
||||
@@ -114,7 +115,7 @@ To change your code to conform use `clang-format -i <your changed files>`.
|
||||
Maintainers are ecosystem participants with elevated access to the repository. They are able to push new code, make decisions on when a release should be made, etc.
|
||||
|
||||
## Code Review
|
||||
PRs must be reviewed by at least one of the maintainers.
|
||||
A PR must be reviewed and approved by at least one of the maintainers before it can be merged.
|
||||
|
||||
## Adding and Removing
|
||||
New maintainers can be proposed by two existing maintainers, subject to a vote by a quorum of the existing maintainers. A minimum of 50% support and a 50% participation is required. In the event of a tie vote, the addition of the new maintainer will be rejected.
|
||||
@@ -123,12 +124,11 @@ Existing maintainers can resign, or be subject to a vote for removal at the behe
|
||||
|
||||
## Existing Maintainers
|
||||
|
||||
* [cjcobb23](https://github.com/cjcobb23) (Ripple)
|
||||
* [legleux](https://github.com/legleux) (Ripple)
|
||||
* [undertome](https://github.com/undertome) (Ripple)
|
||||
* [cindyyan317](https://github.com/cindyyan317) (Ripple)
|
||||
* [godexsoft](https://github.com/godexsoft) (Ripple)
|
||||
* [officialfrancismendoza](https://github.com/officialfrancismendoza) (Ripple)
|
||||
* [legleux](https://github.com/legleux) (Ripple)
|
||||
|
||||
## Honorable ex-Maintainers
|
||||
|
||||
* [cjcobb23](https://github.com/cjcobb23) (ex-Ripple)
|
||||
* [natenichols](https://github.com/natenichols) (ex-Ripple)
|
||||
|
||||
65
README.md
65
README.md
@@ -22,13 +22,15 @@ from which data can be extracted. The rippled node does not need to be running o
|
||||
|
||||
## Building
|
||||
|
||||
Clio is built with CMake. Clio requires at least GCC-11 (C++20), and Boost 1.75.0 or later.
|
||||
Clio is built with CMake. Clio requires at least GCC-11/clang-14.0.0 (C++20), and Boost 1.75.0.
|
||||
|
||||
Use these instructions to build a Clio executable from the source. These instructions were tested on Ubuntu 20.04 LTS.
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
sudo apt-get -y install git pkg-config protobuf-compiler libprotobuf-dev libssl-dev wget build-essential bison flex autoconf cmake clang-format
|
||||
# Install gcovr to run code coverage
|
||||
sudo apt-get -y install gcovr
|
||||
|
||||
# Compile Boost
|
||||
wget -O $HOME/boost_1_75_0.tar.gz https://boostorg.jfrog.io/artifactory/main/release/1.75.0/source/boost_1_75_0.tar.gz
|
||||
@@ -72,6 +74,36 @@ server is running
|
||||
to the IP of your Clio server. This entry can take the form of a comma-separated list if
|
||||
you are running multiple Clio nodes.
|
||||
|
||||
|
||||
In addition, the parameter `start_sequence` can be included and configured within the top level of the config file. This parameter specifies the sequence of first ledger to extract if the database is empty. Note that ETL extracts ledgers in order and that no backfilling functionality currently exists, meaning Clio will not retroactively learn ledgers older than the one you specify. Choosing to specify this or not will yield the following behavior:
|
||||
- If this setting is absent and the database is empty, ETL will start with the next ledger validated by the network.
|
||||
- If this setting is present and the database is not empty, an exception is thrown.
|
||||
|
||||
In addition, the optional parameter `finish_sequence` can be added to the json file as well, specifying where the ledger can stop.
|
||||
|
||||
To add `start_sequence` and/or `finish_sequence` to the config.json file appropriately, they will be on the same top level of precedence as other parameters (such as `database`, `etl_sources`, `read_only`, etc.) and be specified with an integer. Here is an example snippet from the config file:
|
||||
|
||||
```json
|
||||
"start_sequence": 12345,
|
||||
"finish_sequence": 54321
|
||||
```
|
||||
|
||||
The parameters `ssl_cert_file` and `ssl_key_file` can also be added to the top level of precedence of our Clio config. `ssl_cert_file` specifies the filepath for your SSL cert while `ssl_key_file` specifies the filepath for your SSL key. It is up to you how to change ownership of these folders for your designated Clio user. Your options include:
|
||||
- Copying the two files as root somewhere that's accessible by the Clio user, then running `sudo chown` to your user
|
||||
- Changing the permissions directly so it's readable by your Clio user
|
||||
- Running Clio as root (strongly discouraged)
|
||||
|
||||
An example of how to specify `ssl_cert_file` and `ssl_key_file` in the config:
|
||||
|
||||
```json
|
||||
"server":{
|
||||
"ip": "0.0.0.0",
|
||||
"port": 51233
|
||||
},
|
||||
"ssl_cert_file" : "/full/path/to/cert.file",
|
||||
"ssl_key_file" : "/full/path/to/key.file"
|
||||
```
|
||||
|
||||
Once your config files are ready, start rippled and Clio. It doesn't matter which you
|
||||
start first, and it's fine to stop one or the other and restart at any given time.
|
||||
|
||||
@@ -152,9 +184,33 @@ You must:
|
||||
## Logging
|
||||
Clio provides several logging options, all are configurable via the config file and are detailed below.
|
||||
|
||||
`log_level`: The minimum level of severity at which the log message will be outputted.
|
||||
`log_level`: The minimum level of severity at which the log message will be outputted by default.
|
||||
Severity options are `trace`, `debug`, `info`, `warning`, `error`, `fatal`. Defaults to `info`.
|
||||
|
||||
`log_format`: The format of log lines produced by clio. Defaults to `"%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%"`.
|
||||
Each of the variables expands like so
|
||||
- `TimeStamp`: The full date and time of the log entry
|
||||
- `SourceLocation`: A partial path to the c++ file and the line number in said file (`source/file/path:linenumber`)
|
||||
- `ThreadID`: The ID of the thread the log entry is written from
|
||||
- `Channel`: The channel that this log entry was sent to
|
||||
- `Severity`: The severity (aka log level) the entry was sent at
|
||||
- `Message`: The actual log message
|
||||
|
||||
`log_channels`: An array of json objects, each overriding properties for a logging `channel`.
|
||||
At the moment of writing, only `log_level` can be overriden using this mechanism.
|
||||
|
||||
Each object is of this format:
|
||||
```json
|
||||
{
|
||||
"channel": "Backend",
|
||||
"log_level": "fatal"
|
||||
}
|
||||
```
|
||||
If no override is present for a given channel, that channel will log at the severity specified by the global `log_level`.
|
||||
Overridable log channels: `Backend`, `WebServer`, `Subscriptions`, `RPC`, `ETL` and `Performance`.
|
||||
|
||||
> **Note:** See `example-config.json` for more details.
|
||||
|
||||
`log_to_console`: Enable/disable log output to console. Options are `true`/`false`. Defaults to true.
|
||||
|
||||
`log_directory`: Path to the directory where log files are stored. If such directory doesn't exist, Clio will create it. If not specified, logs are not written to a file.
|
||||
@@ -170,6 +226,11 @@ rotate the current log file. Defaults to 12 hours.
|
||||
Note, time-based log rotation occurs dependently on size-based log rotation, where if a
|
||||
size-based log rotation occurs, the timer for the time-based rotation will reset.
|
||||
|
||||
`log_tag_style`: Tag implementation to use. Must be one of:
|
||||
- `uint`: Lock free and threadsafe but outputs just a simple unsigned integer
|
||||
- `uuid`: Threadsafe and outputs a UUID tag
|
||||
- `none`: Don't use tagging at all
|
||||
|
||||
## Cassandra / Scylla Administration
|
||||
|
||||
Since Clio relies on either Cassandra or Scylla for its database backend, here are some important considerations:
|
||||
|
||||
@@ -1,46 +1,93 @@
|
||||
{
|
||||
"database":
|
||||
{
|
||||
"type":"cassandra",
|
||||
"cassandra":
|
||||
{
|
||||
"contact_points":"127.0.0.1",
|
||||
"port":9042,
|
||||
"keyspace":"clio",
|
||||
"replication_factor":1,
|
||||
"table_prefix":"",
|
||||
"max_write_requests_outstanding":25000,
|
||||
"max_read_requests_outstanding":30000,
|
||||
"threads":8
|
||||
"database": {
|
||||
"type": "cassandra",
|
||||
"cassandra": {
|
||||
"contact_points": "127.0.0.1",
|
||||
"port": 9042,
|
||||
"keyspace": "clio",
|
||||
"replication_factor": 1,
|
||||
"table_prefix": "",
|
||||
"max_write_requests_outstanding": 25000,
|
||||
"max_read_requests_outstanding": 30000,
|
||||
"threads": 8
|
||||
}
|
||||
},
|
||||
"etl_sources":
|
||||
[
|
||||
"etl_sources": [
|
||||
{
|
||||
"ip":"127.0.0.1",
|
||||
"ws_port":"6006",
|
||||
"grpc_port":"50051"
|
||||
"ip": "127.0.0.1",
|
||||
"ws_port": "6006",
|
||||
"grpc_port": "50051"
|
||||
}
|
||||
],
|
||||
"dos_guard":
|
||||
{
|
||||
"whitelist":["127.0.0.1"]
|
||||
"dos_guard": {
|
||||
"whitelist": [
|
||||
"127.0.0.1"
|
||||
], // comma-separated list of ips to exclude from rate limiting
|
||||
/* The below values are the default values and are only specified here
|
||||
* for documentation purposes. The rate limiter currently limits
|
||||
* connections and bandwidth per ip. The rate limiter looks at the raw
|
||||
* ip of a client connection, and so requests routed through a load
|
||||
* balancer will all have the same ip and be treated as a single client
|
||||
*/
|
||||
"max_fetches": 1000000, // max bytes per ip per sweep interval
|
||||
"max_connections": 20, // max connections per ip
|
||||
"max_requests": 20, // max connections per ip
|
||||
"sweep_interval": 1 // time in seconds before resetting bytes per ip count
|
||||
},
|
||||
"cache":
|
||||
{
|
||||
"peers": [{"ip":"127.0.0.1","port":51234}]
|
||||
"cache": {
|
||||
"peers": [
|
||||
{
|
||||
"ip": "127.0.0.1",
|
||||
"port": 51234
|
||||
}
|
||||
]
|
||||
},
|
||||
"server":{
|
||||
"ip":"0.0.0.0",
|
||||
"port":51233
|
||||
"server": {
|
||||
"ip": "0.0.0.0",
|
||||
"port": 51233,
|
||||
/* Max number of requests to queue up before rejecting further requests.
|
||||
* Defaults to 0, which disables the limit
|
||||
*/
|
||||
"max_queue_size": 500
|
||||
},
|
||||
"log_level":"debug",
|
||||
"log_channels": [
|
||||
{
|
||||
"channel": "Backend",
|
||||
"log_level": "fatal"
|
||||
},
|
||||
{
|
||||
"channel": "WebServer",
|
||||
"log_level": "info"
|
||||
},
|
||||
{
|
||||
"channel": "Subscriptions",
|
||||
"log_level": "info"
|
||||
},
|
||||
{
|
||||
"channel": "RPC",
|
||||
"log_level": "error"
|
||||
},
|
||||
{
|
||||
"channel": "ETL",
|
||||
"log_level": "debug"
|
||||
},
|
||||
{
|
||||
"channel": "Performance",
|
||||
"log_level": "trace"
|
||||
}
|
||||
],
|
||||
"log_level": "info",
|
||||
"log_format": "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%", // This is the default format
|
||||
"log_to_console": true,
|
||||
"log_directory":"./clio_log",
|
||||
"log_directory": "./clio_log",
|
||||
"log_rotation_size": 2048,
|
||||
"log_directory_max_size": 51200,
|
||||
"log_rotation_hour_interval": 12,
|
||||
"log_tag_style": "uint",
|
||||
"extractor_threads":8,
|
||||
"read_only":false
|
||||
"extractor_threads": 8,
|
||||
"read_only": false,
|
||||
//"start_sequence": [integer] the ledger index to start from,
|
||||
//"finish_sequence": [integer] the ledger index to finish at,
|
||||
//"ssl_cert_file" : "/full/path/to/cert.file",
|
||||
//"ssl_key_file" : "/full/path/to/key.file"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_BACKENDFACTORY_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_BACKENDFACTORY_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <backend/CassandraBackend.h>
|
||||
#include <backend/CassandraBackendNew.h>
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace Backend {
|
||||
std::shared_ptr<BackendInterface>
|
||||
make_Backend(boost::asio::io_context& ioc, clio::Config const& config)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __func__ << ": Constructing BackendInterface";
|
||||
static clio::Logger log{"Backend"};
|
||||
log.info() << "Constructing BackendInterface";
|
||||
|
||||
auto readOnly = config.valueOr("read_only", false);
|
||||
auto type = config.value<std::string>("database.type");
|
||||
@@ -22,6 +44,13 @@ make_Backend(boost::asio::io_context& ioc, clio::Config const& config)
|
||||
auto ttl = config.valueOr<uint32_t>("online_delete", 0) * 4;
|
||||
backend = std::make_shared<CassandraBackend>(ioc, cfg, ttl);
|
||||
}
|
||||
else if (boost::iequals(type, "cassandra-new"))
|
||||
{
|
||||
auto cfg = config.section("database." + type);
|
||||
auto ttl = config.valueOr<uint16_t>("online_delete", 0) * 4;
|
||||
backend =
|
||||
std::make_shared<Backend::Cassandra::CassandraBackend>(Backend::Cassandra::SettingsProvider{cfg, ttl});
|
||||
}
|
||||
|
||||
if (!backend)
|
||||
throw std::runtime_error("Invalid database type");
|
||||
@@ -34,11 +63,8 @@ make_Backend(boost::asio::io_context& ioc, clio::Config const& config)
|
||||
backend->updateRange(rng->maxSequence);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< __func__ << ": Constructed BackendInterface Successfully";
|
||||
log.info() << "Constructed BackendInterface Successfully";
|
||||
|
||||
return backend;
|
||||
}
|
||||
} // namespace Backend
|
||||
|
||||
#endif // RIPPLE_REPORTING_BACKEND_FACTORY
|
||||
|
||||
@@ -1,32 +1,58 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/STLedgerEntry.h>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
using namespace clio;
|
||||
|
||||
// local to compilation unit loggers
|
||||
namespace {
|
||||
clio::Logger gLog{"Backend"};
|
||||
} // namespace
|
||||
|
||||
namespace Backend {
|
||||
bool
|
||||
BackendInterface::finishWrites(std::uint32_t const ledgerSequence)
|
||||
{
|
||||
gLog.debug() << "Want finish writes for " << ledgerSequence;
|
||||
auto commitRes = doFinishWrites();
|
||||
if (commitRes)
|
||||
{
|
||||
gLog.debug() << "Successfully commited. Updating range now to " << ledgerSequence;
|
||||
updateRange(ledgerSequence);
|
||||
}
|
||||
return commitRes;
|
||||
}
|
||||
void
|
||||
BackendInterface::writeLedgerObject(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& blob)
|
||||
BackendInterface::writeLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob)
|
||||
{
|
||||
assert(key.size() == sizeof(ripple::uint256));
|
||||
doWriteLedgerObject(std::move(key), seq, std::move(blob));
|
||||
}
|
||||
|
||||
std::optional<LedgerRange>
|
||||
BackendInterface::hardFetchLedgerRangeNoThrow(
|
||||
boost::asio::yield_context& yield) const
|
||||
BackendInterface::hardFetchLedgerRangeNoThrow(boost::asio::yield_context& yield) const
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__ << "(yield)";
|
||||
gLog.trace() << "called";
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@@ -43,7 +69,7 @@ BackendInterface::hardFetchLedgerRangeNoThrow(
|
||||
std::optional<LedgerRange>
|
||||
BackendInterface::hardFetchLedgerRangeNoThrow() const
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__ << "()";
|
||||
gLog.trace() << "called";
|
||||
return retryOnTimeout([&]() { return hardFetchLedgerRange(); });
|
||||
}
|
||||
|
||||
@@ -57,21 +83,17 @@ BackendInterface::fetchLedgerObject(
|
||||
auto obj = cache_.get(key, sequence);
|
||||
if (obj)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - cache hit - " << ripple::strHex(key);
|
||||
gLog.trace() << "Cache hit - " << ripple::strHex(key);
|
||||
return *obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - cache miss - " << ripple::strHex(key);
|
||||
gLog.trace() << "Cache miss - " << ripple::strHex(key);
|
||||
auto dbObj = doFetchLedgerObject(key, sequence, yield);
|
||||
if (!dbObj)
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - missed cache and missed in db";
|
||||
gLog.trace() << "Missed cache and missed in db";
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - missed cache but found in db";
|
||||
gLog.trace() << "Missed cache but found in db";
|
||||
return dbObj;
|
||||
}
|
||||
}
|
||||
@@ -93,9 +115,7 @@ BackendInterface::fetchLedgerObjects(
|
||||
else
|
||||
misses.push_back(keys[i]);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - cache hits = " << keys.size() - misses.size()
|
||||
<< " - cache misses = " << misses.size();
|
||||
gLog.trace() << "Cache hits = " << keys.size() - misses.size() << " - cache misses = " << misses.size();
|
||||
|
||||
if (misses.size())
|
||||
{
|
||||
@@ -121,11 +141,9 @@ BackendInterface::fetchSuccessorKey(
|
||||
{
|
||||
auto succ = cache_.getSuccessor(key, ledgerSequence);
|
||||
if (succ)
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - cache hit - " << ripple::strHex(key);
|
||||
gLog.trace() << "Cache hit - " << ripple::strHex(key);
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " - cache miss - " << ripple::strHex(key);
|
||||
gLog.trace() << "Cache miss - " << ripple::strHex(key);
|
||||
return succ ? succ->key : doFetchSuccessorKey(key, ledgerSequence, yield);
|
||||
}
|
||||
|
||||
@@ -152,7 +170,6 @@ BackendInterface::fetchBookOffers(
|
||||
ripple::uint256 const& book,
|
||||
std::uint32_t const ledgerSequence,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::uint256> const& cursor,
|
||||
boost::asio::yield_context& yield) const
|
||||
{
|
||||
// TODO try to speed this up. This can take a few seconds. The goal is
|
||||
@@ -161,10 +178,7 @@ BackendInterface::fetchBookOffers(
|
||||
const ripple::uint256 bookEnd = ripple::getQualityNext(book);
|
||||
ripple::uint256 uTipIndex = book;
|
||||
std::vector<ripple::uint256> keys;
|
||||
auto getMillis = [](auto diff) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(diff)
|
||||
.count();
|
||||
};
|
||||
auto getMillis = [](auto diff) { return std::chrono::duration_cast<std::chrono::milliseconds>(diff).count(); };
|
||||
auto begin = std::chrono::system_clock::now();
|
||||
std::uint32_t numSucc = 0;
|
||||
std::uint32_t numPages = 0;
|
||||
@@ -179,30 +193,24 @@ BackendInterface::fetchBookOffers(
|
||||
succMillis += getMillis(mid2 - mid1);
|
||||
if (!offerDir || offerDir->key >= bookEnd)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__ << " - offerDir.has_value() "
|
||||
<< offerDir.has_value() << " breaking";
|
||||
gLog.trace() << "offerDir.has_value() " << offerDir.has_value() << " breaking";
|
||||
break;
|
||||
}
|
||||
uTipIndex = offerDir->key;
|
||||
while (keys.size() < limit)
|
||||
{
|
||||
++numPages;
|
||||
ripple::STLedgerEntry sle{
|
||||
ripple::SerialIter{
|
||||
offerDir->blob.data(), offerDir->blob.size()},
|
||||
offerDir->key};
|
||||
ripple::STLedgerEntry sle{ripple::SerialIter{offerDir->blob.data(), offerDir->blob.size()}, offerDir->key};
|
||||
auto indexes = sle.getFieldV256(ripple::sfIndexes);
|
||||
keys.insert(keys.end(), indexes.begin(), indexes.end());
|
||||
auto next = sle.getFieldU64(ripple::sfIndexNext);
|
||||
if (!next)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " next is empty. breaking";
|
||||
gLog.trace() << "Next is empty. breaking";
|
||||
break;
|
||||
}
|
||||
auto nextKey = ripple::keylet::page(uTipIndex, next);
|
||||
auto nextDir =
|
||||
fetchLedgerObject(nextKey.key, ledgerSequence, yield);
|
||||
auto nextDir = fetchLedgerObject(nextKey.key, ledgerSequence, yield);
|
||||
assert(nextDir);
|
||||
offerDir->blob = *nextDir;
|
||||
offerDir->key = nextKey.key;
|
||||
@@ -214,29 +222,21 @@ BackendInterface::fetchBookOffers(
|
||||
auto objs = fetchLedgerObjects(keys, ledgerSequence, yield);
|
||||
for (size_t i = 0; i < keys.size() && i < limit; ++i)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< __func__ << " key = " << ripple::strHex(keys[i])
|
||||
<< " blob = " << ripple::strHex(objs[i])
|
||||
<< " ledgerSequence = " << ledgerSequence;
|
||||
gLog.trace() << "Key = " << ripple::strHex(keys[i]) << " blob = " << ripple::strHex(objs[i])
|
||||
<< " ledgerSequence = " << ledgerSequence;
|
||||
assert(objs[i].size());
|
||||
page.offers.push_back({keys[i], objs[i]});
|
||||
}
|
||||
auto end = std::chrono::system_clock::now();
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< __func__ << " "
|
||||
<< "Fetching " << std::to_string(keys.size()) << " offers took "
|
||||
<< std::to_string(getMillis(mid - begin))
|
||||
<< " milliseconds. Fetching next dir took "
|
||||
<< std::to_string(succMillis) << " milliseonds. Fetched next dir "
|
||||
<< std::to_string(numSucc) << " times"
|
||||
<< " Fetching next page of dir took " << std::to_string(pageMillis)
|
||||
<< " milliseconds"
|
||||
<< ". num pages = " << std::to_string(numPages)
|
||||
<< ". Fetching all objects took "
|
||||
<< std::to_string(getMillis(end - mid))
|
||||
<< " milliseconds. total time = "
|
||||
<< std::to_string(getMillis(end - begin)) << " milliseconds"
|
||||
<< " book = " << ripple::strHex(book);
|
||||
gLog.debug() << "Fetching " << std::to_string(keys.size()) << " offers took "
|
||||
<< std::to_string(getMillis(mid - begin)) << " milliseconds. Fetching next dir took "
|
||||
<< std::to_string(succMillis) << " milliseonds. Fetched next dir " << std::to_string(numSucc)
|
||||
<< " times"
|
||||
<< " Fetching next page of dir took " << std::to_string(pageMillis) << " milliseconds"
|
||||
<< ". num pages = " << std::to_string(numPages) << ". Fetching all objects took "
|
||||
<< std::to_string(getMillis(end - mid))
|
||||
<< " milliseconds. total time = " << std::to_string(getMillis(end - begin)) << " milliseconds"
|
||||
<< " book = " << ripple::strHex(book);
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -255,11 +255,8 @@ BackendInterface::fetchLedgerPage(
|
||||
bool reachedEnd = false;
|
||||
while (keys.size() < limit && !reachedEnd)
|
||||
{
|
||||
ripple::uint256 const& curCursor = keys.size() ? keys.back()
|
||||
: cursor ? *cursor
|
||||
: firstKey;
|
||||
std::uint32_t const seq =
|
||||
outOfOrder ? range->maxSequence : ledgerSequence;
|
||||
ripple::uint256 const& curCursor = keys.size() ? keys.back() : cursor ? *cursor : firstKey;
|
||||
std::uint32_t const seq = outOfOrder ? range->maxSequence : ledgerSequence;
|
||||
auto succ = fetchSuccessorKey(curCursor, seq, yield);
|
||||
if (!succ)
|
||||
reachedEnd = true;
|
||||
@@ -274,16 +271,14 @@ BackendInterface::fetchLedgerPage(
|
||||
page.objects.push_back({std::move(keys[i]), std::move(objects[i])});
|
||||
else if (!outOfOrder)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< __func__
|
||||
<< " deleted or non-existent object in successor table. key = "
|
||||
<< ripple::strHex(keys[i]) << " - seq = " << ledgerSequence;
|
||||
gLog.error() << "Deleted or non-existent object in successor table. key = " << ripple::strHex(keys[i])
|
||||
<< " - seq = " << ledgerSequence;
|
||||
std::stringstream msg;
|
||||
for (size_t j = 0; j < objects.size(); ++j)
|
||||
{
|
||||
msg << " - " << ripple::strHex(keys[j]);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << msg.str();
|
||||
gLog.error() << msg.str();
|
||||
}
|
||||
}
|
||||
if (keys.size() && !reachedEnd)
|
||||
@@ -293,9 +288,7 @@ BackendInterface::fetchLedgerPage(
|
||||
}
|
||||
|
||||
std::optional<ripple::Fees>
|
||||
BackendInterface::fetchFees(
|
||||
std::uint32_t const seq,
|
||||
boost::asio::yield_context& yield) const
|
||||
BackendInterface::fetchFees(std::uint32_t const seq, boost::asio::yield_context& yield) const
|
||||
{
|
||||
ripple::Fees fees;
|
||||
|
||||
@@ -304,7 +297,7 @@ BackendInterface::fetchFees(
|
||||
|
||||
if (!bytes)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " - could not find fees";
|
||||
gLog.error() << "Could not find fees";
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_BACKENDINTERFACE_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_BACKENDINTERFACE_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
|
||||
#include <backend/DBHelpers.h>
|
||||
#include <backend/SimpleCache.h>
|
||||
#include <backend/Types.h>
|
||||
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
@@ -46,6 +62,8 @@ template <class F>
|
||||
auto
|
||||
retryOnTimeout(F func, size_t waitMs = 500)
|
||||
{
|
||||
static clio::Logger log{"Backend"};
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@@ -54,9 +72,7 @@ retryOnTimeout(F func, size_t waitMs = 500)
|
||||
}
|
||||
catch (DatabaseTimeout& t)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< __func__
|
||||
<< " Database request timed out. Sleeping and retrying ... ";
|
||||
log.error() << "Database request timed out. Sleeping and retrying ... ";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
|
||||
}
|
||||
}
|
||||
@@ -94,7 +110,7 @@ synchronous(F&& f)
|
||||
* R is the currently executing coroutine that is about to get passed.
|
||||
* If corountine types do not match, the current one's type is stored.
|
||||
*/
|
||||
using R = typename std::result_of<F(boost::asio::yield_context&)>::type;
|
||||
using R = typename boost::result_of<F(boost::asio::yield_context&)>::type;
|
||||
if constexpr (!std::is_same<R, void>::value)
|
||||
{
|
||||
/**
|
||||
@@ -105,11 +121,10 @@ synchronous(F&& f)
|
||||
* executing coroutine, yield. The different type is returned.
|
||||
*/
|
||||
R res;
|
||||
boost::asio::spawn(
|
||||
strand, [&f, &work, &res](boost::asio::yield_context yield) {
|
||||
res = f(yield);
|
||||
work.reset();
|
||||
});
|
||||
boost::asio::spawn(strand, [&f, &work, &res](boost::asio::yield_context yield) {
|
||||
res = f(yield);
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
return res;
|
||||
@@ -117,11 +132,10 @@ synchronous(F&& f)
|
||||
else
|
||||
{
|
||||
/*! @brief When the corutine type is different, run as normal. */
|
||||
boost::asio::spawn(
|
||||
strand, [&f, &work](boost::asio::yield_context yield) {
|
||||
f(yield);
|
||||
work.reset();
|
||||
});
|
||||
boost::asio::spawn(strand, [&f, &work](boost::asio::yield_context yield) {
|
||||
f(yield);
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
}
|
||||
@@ -165,12 +179,8 @@ protected:
|
||||
*/
|
||||
|
||||
public:
|
||||
BackendInterface(clio::Config const& config)
|
||||
{
|
||||
}
|
||||
virtual ~BackendInterface()
|
||||
{
|
||||
}
|
||||
BackendInterface() = default;
|
||||
virtual ~BackendInterface() = default;
|
||||
|
||||
/*! @brief LEDGER METHODS */
|
||||
public:
|
||||
@@ -196,15 +206,11 @@ public:
|
||||
|
||||
/*! @brief Fetches a specific ledger by sequence number. */
|
||||
virtual std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerBySequence(
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/*! @brief Fetches a specific ledger by hash. */
|
||||
virtual std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerByHash(
|
||||
ripple::uint256 const& hash,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchLedgerByHash(ripple::uint256 const& hash, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/*! @brief Fetches the latest ledger sequence. */
|
||||
virtual std::optional<std::uint32_t>
|
||||
@@ -229,7 +235,7 @@ public:
|
||||
void
|
||||
updateRange(uint32_t newMax)
|
||||
{
|
||||
std::unique_lock lck(rngMtx_);
|
||||
std::scoped_lock lck(rngMtx_);
|
||||
assert(!range || newMax >= range->maxSequence);
|
||||
if (!range)
|
||||
range = {newMax, newMax};
|
||||
@@ -256,9 +262,7 @@ public:
|
||||
* @return std::optional<TransactionAndMetadata>
|
||||
*/
|
||||
virtual std::optional<TransactionAndMetadata>
|
||||
fetchTransaction(
|
||||
ripple::uint256 const& hash,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches multiple transactions.
|
||||
@@ -268,9 +272,7 @@ public:
|
||||
* @return std::vector<TransactionAndMetadata>
|
||||
*/
|
||||
virtual std::vector<TransactionAndMetadata>
|
||||
fetchTransactions(
|
||||
std::vector<ripple::uint256> const& hashes,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches all transactions for a specific account
|
||||
@@ -301,9 +303,7 @@ public:
|
||||
* @return std::vector<TransactionAndMetadata>
|
||||
*/
|
||||
virtual std::vector<TransactionAndMetadata>
|
||||
fetchAllTransactionsInLedger(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches all transaction hashes from a specific ledger.
|
||||
@@ -313,9 +313,7 @@ public:
|
||||
* @return std::vector<ripple::uint256>
|
||||
*/
|
||||
virtual std::vector<ripple::uint256>
|
||||
fetchAllTransactionHashesInLedger(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchAllTransactionHashesInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/*! @brief NFT methods */
|
||||
/**
|
||||
@@ -327,10 +325,8 @@ public:
|
||||
* @return std::optional<NFT>
|
||||
*/
|
||||
virtual std::optional<NFT>
|
||||
fetchNFT(
|
||||
ripple::uint256 const& tokenID,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches all transactions for a specific NFT.
|
||||
@@ -360,10 +356,8 @@ public:
|
||||
* @return std::optional<Blob>
|
||||
*/
|
||||
std::optional<Blob>
|
||||
fetchLedgerObject(
|
||||
ripple::uint256 const& key,
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const;
|
||||
fetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context& yield)
|
||||
const;
|
||||
|
||||
/**
|
||||
* @brief Fetches all ledger objects: a vector of vectors of unsigned chars.
|
||||
@@ -381,10 +375,8 @@ public:
|
||||
|
||||
/*! @brief Virtual function version of fetchLedgerObject */
|
||||
virtual std::optional<Blob>
|
||||
doFetchLedgerObject(
|
||||
ripple::uint256 const& key,
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context& yield)
|
||||
const = 0;
|
||||
|
||||
/*! @brief Virtual function version of fetchLedgerObjects */
|
||||
virtual std::vector<Blob>
|
||||
@@ -404,9 +396,7 @@ public:
|
||||
* @return std::vector<LedgerObject>
|
||||
*/
|
||||
virtual std::vector<LedgerObject>
|
||||
fetchLedgerDiff(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches a page of ledger objects, ordered by key/index.
|
||||
@@ -428,24 +418,17 @@ public:
|
||||
|
||||
/*! @brief Fetches successor object from key/index. */
|
||||
std::optional<LedgerObject>
|
||||
fetchSuccessorObject(
|
||||
ripple::uint256 key,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const;
|
||||
fetchSuccessorObject(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const;
|
||||
|
||||
/*! @brief Fetches successor key from key/index. */
|
||||
std::optional<ripple::uint256>
|
||||
fetchSuccessorKey(
|
||||
ripple::uint256 key,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const;
|
||||
fetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const;
|
||||
|
||||
/*! @brief Virtual function version of fetchSuccessorKey. */
|
||||
virtual std::optional<ripple::uint256>
|
||||
doFetchSuccessorKey(
|
||||
ripple::uint256 key,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches book offers.
|
||||
@@ -462,7 +445,6 @@ public:
|
||||
ripple::uint256 const& book,
|
||||
std::uint32_t const ledgerSequence,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::uint256> const& cursor,
|
||||
boost::asio::yield_context& yield) const;
|
||||
|
||||
/**
|
||||
@@ -478,9 +460,7 @@ public:
|
||||
std::optional<LedgerRange>
|
||||
hardFetchLedgerRange() const
|
||||
{
|
||||
return synchronous([&](boost::asio::yield_context yield) {
|
||||
return hardFetchLedgerRange(yield);
|
||||
});
|
||||
return synchronous([&](boost::asio::yield_context yield) { return hardFetchLedgerRange(yield); });
|
||||
}
|
||||
|
||||
/*! @brief Virtual function equivalent of hardFetchLedgerRange. */
|
||||
@@ -501,9 +481,7 @@ public:
|
||||
* @param ledgerHeader r-value string representing ledger header.
|
||||
*/
|
||||
virtual void
|
||||
writeLedger(
|
||||
ripple::LedgerInfo const& ledgerInfo,
|
||||
std::string&& ledgerHeader) = 0;
|
||||
writeLedger(ripple::LedgerInfo const& ledgerInfo, std::string&& ledgerHeader) = 0;
|
||||
|
||||
/**
|
||||
* @brief Writes a new ledger object.
|
||||
@@ -515,10 +493,7 @@ public:
|
||||
* @param blob r-value vector of unsigned characters (blob).
|
||||
*/
|
||||
virtual void
|
||||
writeLedgerObject(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& blob);
|
||||
writeLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob);
|
||||
|
||||
/**
|
||||
* @brief Writes a new transaction.
|
||||
@@ -569,10 +544,7 @@ public:
|
||||
* @param successor Passed in as an r-value reference.
|
||||
*/
|
||||
virtual void
|
||||
writeSuccessor(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& successor) = 0;
|
||||
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) = 0;
|
||||
|
||||
/*! @brief Tells database we will write data for a specific ledger. */
|
||||
virtual void
|
||||
@@ -601,9 +573,7 @@ public:
|
||||
* @return false
|
||||
*/
|
||||
virtual bool
|
||||
doOnlineDelete(
|
||||
std::uint32_t numLedgersToKeep,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
doOnlineDelete(std::uint32_t numLedgersToKeep, boost::asio::yield_context& yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Opens the database
|
||||
@@ -633,10 +603,7 @@ private:
|
||||
* @param blob r-value vector of unsigned chars.
|
||||
*/
|
||||
virtual void
|
||||
doWriteLedgerObject(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& blob) = 0;
|
||||
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) = 0;
|
||||
|
||||
virtual bool
|
||||
doFinishWrites() = 0;
|
||||
@@ -644,4 +611,3 @@ private:
|
||||
|
||||
} // namespace Backend
|
||||
using BackendInterface = Backend::BackendInterface;
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,38 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_CASSANDRABACKEND_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_CASSANDRABACKEND_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <backend/DBHelpers.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/async_result.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <backend/DBHelpers.h>
|
||||
#include <cassandra.h>
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -25,6 +46,7 @@ namespace Backend {
|
||||
class CassandraPreparedStatement
|
||||
{
|
||||
private:
|
||||
clio::Logger log_{"Backend"};
|
||||
CassPrepared const* prepared_ = nullptr;
|
||||
|
||||
public:
|
||||
@@ -63,9 +85,9 @@ public:
|
||||
else
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "nodestore: Error preparing statement : " << rc << ", "
|
||||
<< cass_error_desc(rc) << ". query : " << query;
|
||||
BOOST_LOG_TRIVIAL(error) << ss.str();
|
||||
ss << "nodestore: Error preparing statement : " << rc << ", " << cass_error_desc(rc)
|
||||
<< ". query : " << query;
|
||||
log_.error() << ss.str();
|
||||
}
|
||||
cass_future_free(prepareFuture);
|
||||
return rc == CASS_OK;
|
||||
@@ -73,7 +95,7 @@ public:
|
||||
|
||||
~CassandraPreparedStatement()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__;
|
||||
log_.trace() << "called";
|
||||
if (prepared_)
|
||||
{
|
||||
cass_prepared_free(prepared_);
|
||||
@@ -86,6 +108,7 @@ class CassandraStatement
|
||||
{
|
||||
CassStatement* statement_ = nullptr;
|
||||
size_t curBindingIndex_ = 0;
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
public:
|
||||
CassandraStatement(CassandraPreparedStatement const& prepared)
|
||||
@@ -114,16 +137,13 @@ public:
|
||||
bindNextBoolean(bool val)
|
||||
{
|
||||
if (!statement_)
|
||||
throw std::runtime_error(
|
||||
"CassandraStatement::bindNextBoolean - statement_ is null");
|
||||
CassError rc = cass_statement_bind_bool(
|
||||
statement_, curBindingIndex_, static_cast<cass_bool_t>(val));
|
||||
throw std::runtime_error("CassandraStatement::bindNextBoolean - statement_ is null");
|
||||
CassError rc = cass_statement_bind_bool(statement_, curBindingIndex_, static_cast<cass_bool_t>(val));
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding boolean to statement: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding boolean to statement: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
curBindingIndex_++;
|
||||
@@ -167,19 +187,14 @@ public:
|
||||
bindNextBytes(const unsigned char* data, std::uint32_t const size)
|
||||
{
|
||||
if (!statement_)
|
||||
throw std::runtime_error(
|
||||
"CassandraStatement::bindNextBytes - statement_ is null");
|
||||
CassError rc = cass_statement_bind_bytes(
|
||||
statement_,
|
||||
curBindingIndex_,
|
||||
static_cast<cass_byte_t const*>(data),
|
||||
size);
|
||||
throw std::runtime_error("CassandraStatement::bindNextBytes - statement_ is null");
|
||||
CassError rc =
|
||||
cass_statement_bind_bytes(statement_, curBindingIndex_, static_cast<cass_byte_t const*>(data), size);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding bytes to statement: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding bytes to statement: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
curBindingIndex_++;
|
||||
@@ -189,18 +204,14 @@ public:
|
||||
bindNextUInt(std::uint32_t const value)
|
||||
{
|
||||
if (!statement_)
|
||||
throw std::runtime_error(
|
||||
"CassandraStatement::bindNextUInt - statement_ is null");
|
||||
BOOST_LOG_TRIVIAL(trace)
|
||||
<< std::to_string(curBindingIndex_) << " " << std::to_string(value);
|
||||
CassError rc =
|
||||
cass_statement_bind_int32(statement_, curBindingIndex_, value);
|
||||
throw std::runtime_error("CassandraStatement::bindNextUInt - statement_ is null");
|
||||
log_.trace() << std::to_string(curBindingIndex_) << " " << std::to_string(value);
|
||||
CassError rc = cass_statement_bind_int32(statement_, curBindingIndex_, value);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding uint to statement: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding uint to statement: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
curBindingIndex_++;
|
||||
@@ -216,16 +227,13 @@ public:
|
||||
bindNextInt(int64_t value)
|
||||
{
|
||||
if (!statement_)
|
||||
throw std::runtime_error(
|
||||
"CassandraStatement::bindNextInt - statement_ is null");
|
||||
CassError rc =
|
||||
cass_statement_bind_int64(statement_, curBindingIndex_, value);
|
||||
throw std::runtime_error("CassandraStatement::bindNextInt - statement_ is null");
|
||||
CassError rc = cass_statement_bind_int64(statement_, curBindingIndex_, value);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding int to statement: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding int to statement: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
curBindingIndex_++;
|
||||
@@ -239,27 +247,24 @@ public:
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding int to tuple: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding int to tuple: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
rc = cass_tuple_set_int64(tuple, 1, second);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding int to tuple: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding int to tuple: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
rc = cass_statement_bind_tuple(statement_, curBindingIndex_, tuple);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Error binding tuple to statement: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
|
||||
ss << "Error binding tuple to statement: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << ss.str();
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
cass_tuple_free(tuple);
|
||||
@@ -275,6 +280,7 @@ public:
|
||||
|
||||
class CassandraResult
|
||||
{
|
||||
clio::Logger log_{"Backend"};
|
||||
CassResult const* result_ = nullptr;
|
||||
CassRow const* row_ = nullptr;
|
||||
CassIterator* iter_ = nullptr;
|
||||
@@ -358,14 +364,12 @@ public:
|
||||
throw std::runtime_error("CassandraResult::getBytes - no result");
|
||||
cass_byte_t const* buf;
|
||||
std::size_t bufSize;
|
||||
CassError rc = cass_value_get_bytes(
|
||||
cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
|
||||
CassError rc = cass_value_get_bytes(cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << "CassandraResult::getBytes - error getting value: " << rc
|
||||
<< ", " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << msg.str();
|
||||
msg << "CassandraResult::getBytes - error getting value: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << msg.str();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
curGetIndex_++;
|
||||
@@ -379,14 +383,12 @@ public:
|
||||
throw std::runtime_error("CassandraResult::uint256 - no result");
|
||||
cass_byte_t const* buf;
|
||||
std::size_t bufSize;
|
||||
CassError rc = cass_value_get_bytes(
|
||||
cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
|
||||
CassError rc = cass_value_get_bytes(cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << "CassandraResult::getuint256 - error getting value: " << rc
|
||||
<< ", " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << msg.str();
|
||||
msg << "CassandraResult::getuint256 - error getting value: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << msg.str();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
curGetIndex_++;
|
||||
@@ -399,14 +401,12 @@ public:
|
||||
if (!row_)
|
||||
throw std::runtime_error("CassandraResult::getInt64 - no result");
|
||||
cass_int64_t val;
|
||||
CassError rc =
|
||||
cass_value_get_int64(cass_row_get_column(row_, curGetIndex_), &val);
|
||||
CassError rc = cass_value_get_int64(cass_row_get_column(row_, curGetIndex_), &val);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << "CassandraResult::getInt64 - error getting value: " << rc
|
||||
<< ", " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << msg.str();
|
||||
msg << "CassandraResult::getInt64 - error getting value: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << msg.str();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
++curGetIndex_;
|
||||
@@ -423,8 +423,7 @@ public:
|
||||
getInt64Tuple()
|
||||
{
|
||||
if (!row_)
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getInt64Tuple - no result");
|
||||
throw std::runtime_error("CassandraResult::getInt64Tuple - no result");
|
||||
|
||||
CassValue const* tuple = cass_row_get_column(row_, curGetIndex_);
|
||||
CassIterator* tupleIter = cass_iterator_from_tuple(tuple);
|
||||
@@ -432,8 +431,7 @@ public:
|
||||
if (!cass_iterator_next(tupleIter))
|
||||
{
|
||||
cass_iterator_free(tupleIter);
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getInt64Tuple - failed to iterate tuple");
|
||||
throw std::runtime_error("CassandraResult::getInt64Tuple - failed to iterate tuple");
|
||||
}
|
||||
|
||||
CassValue const* value = cass_iterator_get_value(tupleIter);
|
||||
@@ -442,8 +440,7 @@ public:
|
||||
if (!cass_iterator_next(tupleIter))
|
||||
{
|
||||
cass_iterator_free(tupleIter);
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getInt64Tuple - failed to iterate tuple");
|
||||
throw std::runtime_error("CassandraResult::getInt64Tuple - failed to iterate tuple");
|
||||
}
|
||||
|
||||
value = cass_iterator_get_value(tupleIter);
|
||||
@@ -462,20 +459,17 @@ public:
|
||||
std::size_t bufSize;
|
||||
|
||||
if (!row_)
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getBytesTuple - no result");
|
||||
throw std::runtime_error("CassandraResult::getBytesTuple - no result");
|
||||
CassValue const* tuple = cass_row_get_column(row_, curGetIndex_);
|
||||
CassIterator* tupleIter = cass_iterator_from_tuple(tuple);
|
||||
if (!cass_iterator_next(tupleIter))
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getBytesTuple - failed to iterate tuple");
|
||||
throw std::runtime_error("CassandraResult::getBytesTuple - failed to iterate tuple");
|
||||
CassValue const* value = cass_iterator_get_value(tupleIter);
|
||||
cass_value_get_bytes(value, &buf, &bufSize);
|
||||
Blob first{buf, buf + bufSize};
|
||||
|
||||
if (!cass_iterator_next(tupleIter))
|
||||
throw std::runtime_error(
|
||||
"CassandraResult::getBytesTuple - failed to iterate tuple");
|
||||
throw std::runtime_error("CassandraResult::getBytesTuple - failed to iterate tuple");
|
||||
value = cass_iterator_get_value(tupleIter);
|
||||
cass_value_get_bytes(value, &buf, &bufSize);
|
||||
Blob second{buf, buf + bufSize};
|
||||
@@ -490,20 +484,17 @@ public:
|
||||
{
|
||||
if (!row_)
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - no result";
|
||||
BOOST_LOG_TRIVIAL(error) << msg.str();
|
||||
throw std::runtime_error(msg.str());
|
||||
std::string msg{"No result"};
|
||||
log_.error() << msg;
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
cass_bool_t val;
|
||||
CassError rc =
|
||||
cass_value_get_bool(cass_row_get_column(row_, curGetIndex_), &val);
|
||||
CassError rc = cass_value_get_bool(cass_row_get_column(row_, curGetIndex_), &val);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - error getting value: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << msg.str();
|
||||
msg << "Error getting value: " << rc << ", " << cass_error_desc(rc);
|
||||
log_.error() << msg.str();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
++curGetIndex_;
|
||||
@@ -518,13 +509,12 @@ public:
|
||||
cass_iterator_free(iter_);
|
||||
}
|
||||
};
|
||||
|
||||
inline bool
|
||||
isTimeout(CassError rc)
|
||||
{
|
||||
if (rc == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or
|
||||
rc == CASS_ERROR_LIB_REQUEST_TIMED_OUT or
|
||||
rc == CASS_ERROR_SERVER_UNAVAILABLE or
|
||||
rc == CASS_ERROR_SERVER_OVERLOADED or
|
||||
if (rc == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or rc == CASS_ERROR_LIB_REQUEST_TIMED_OUT or
|
||||
rc == CASS_ERROR_SERVER_UNAVAILABLE or rc == CASS_ERROR_SERVER_OVERLOADED or
|
||||
rc == CASS_ERROR_SERVER_READ_TIMEOUT)
|
||||
return true;
|
||||
return false;
|
||||
@@ -535,8 +525,7 @@ CassError
|
||||
cass_future_error_code(CassFuture* fut, CompletionToken&& token)
|
||||
{
|
||||
using function_type = void(boost::system::error_code, CassError);
|
||||
using result_type =
|
||||
boost::asio::async_result<CompletionToken, function_type>;
|
||||
using result_type = boost::asio::async_result<CompletionToken, function_type>;
|
||||
using handler_type = typename result_type::completion_handler_type;
|
||||
|
||||
handler_type handler(std::forward<decltype(token)>(token));
|
||||
@@ -555,12 +544,10 @@ cass_future_error_code(CassFuture* fut, CompletionToken&& token)
|
||||
HandlerWrapper* hw = (HandlerWrapper*)data;
|
||||
|
||||
boost::asio::post(
|
||||
boost::asio::get_associated_executor(hw->handler),
|
||||
[fut, hw, handler = std::move(hw->handler)]() mutable {
|
||||
boost::asio::get_associated_executor(hw->handler), [fut, hw, handler = std::move(hw->handler)]() mutable {
|
||||
delete hw;
|
||||
|
||||
handler(
|
||||
boost::system::error_code{}, cass_future_error_code(fut));
|
||||
handler(boost::system::error_code{}, cass_future_error_code(fut));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -585,29 +572,27 @@ private:
|
||||
makeStatement(char const* query, std::size_t params)
|
||||
{
|
||||
CassStatement* ret = cass_statement_new(query, params);
|
||||
CassError rc =
|
||||
cass_statement_set_consistency(ret, CASS_CONSISTENCY_QUORUM);
|
||||
CassError rc = cass_statement_set_consistency(ret, CASS_CONSISTENCY_QUORUM);
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "nodestore: Error setting query consistency: " << query
|
||||
<< ", result: " << rc << ", " << cass_error_desc(rc);
|
||||
ss << "nodestore: Error setting query consistency: " << query << ", result: " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
clio::Logger log_{"Backend"};
|
||||
std::atomic<bool> open_{false};
|
||||
|
||||
std::unique_ptr<CassSession, void (*)(CassSession*)> session_{
|
||||
nullptr,
|
||||
[](CassSession* session) {
|
||||
// Try to disconnect gracefully.
|
||||
CassFuture* fut = cass_session_close(session);
|
||||
cass_future_wait(fut);
|
||||
cass_future_free(fut);
|
||||
cass_session_free(session);
|
||||
}};
|
||||
std::unique_ptr<CassSession, void (*)(CassSession*)> session_{nullptr, [](CassSession* session) {
|
||||
// Try to disconnect gracefully.
|
||||
CassFuture* fut = cass_session_close(session);
|
||||
cass_future_wait(fut);
|
||||
cass_future_free(fut);
|
||||
cass_session_free(session);
|
||||
}};
|
||||
|
||||
// Database statements cached server side. Using these is more efficient
|
||||
// than making a new statement
|
||||
@@ -631,6 +616,8 @@ private:
|
||||
CassandraPreparedStatement insertNFT_;
|
||||
CassandraPreparedStatement selectNFT_;
|
||||
CassandraPreparedStatement insertIssuerNFT_;
|
||||
CassandraPreparedStatement insertNFTURI_;
|
||||
CassandraPreparedStatement selectNFTURI_;
|
||||
CassandraPreparedStatement insertNFTTx_;
|
||||
CassandraPreparedStatement selectNFTTx_;
|
||||
CassandraPreparedStatement selectNFTTxForward_;
|
||||
@@ -678,11 +665,8 @@ private:
|
||||
mutable std::uint32_t ledgerSequence_ = 0;
|
||||
|
||||
public:
|
||||
CassandraBackend(
|
||||
boost::asio::io_context& ioc,
|
||||
clio::Config const& config,
|
||||
uint32_t ttl)
|
||||
: BackendInterface(config), config_(config), ttl_(ttl)
|
||||
CassandraBackend(boost::asio::io_context& ioc, clio::Config const& config, uint32_t ttl)
|
||||
: config_(config), ttl_(ttl)
|
||||
{
|
||||
work_.emplace(ioContext_);
|
||||
ioThread_ = std::thread([this]() { ioContext_.run(); });
|
||||
@@ -751,13 +735,10 @@ public:
|
||||
statement.bindNextInt(ledgerSequence_ - 1);
|
||||
if (!executeSyncUpdate(statement))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< __func__ << " Update failed for ledger "
|
||||
<< std::to_string(ledgerSequence_) << ". Returning";
|
||||
log_.warn() << "Update failed for ledger " << std::to_string(ledgerSequence_) << ". Returning";
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __func__ << " Committed ledger "
|
||||
<< std::to_string(ledgerSequence_);
|
||||
log_.info() << "Committed ledger " << std::to_string(ledgerSequence_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -768,8 +749,7 @@ public:
|
||||
// if db is empty, sync. if sync interval is 1, always sync.
|
||||
// if we've never synced, sync. if its been greater than the configured
|
||||
// sync interval since we last synced, sync.
|
||||
if (!range || lastSync_ == 0 ||
|
||||
ledgerSequence_ - syncInterval_ >= lastSync_)
|
||||
if (!range || lastSync_ == 0 || ledgerSequence_ - syncInterval_ >= lastSync_)
|
||||
{
|
||||
// wait for all other writes to finish
|
||||
sync();
|
||||
@@ -791,22 +771,16 @@ public:
|
||||
statement.bindNextInt(lastSync_);
|
||||
if (!executeSyncUpdate(statement))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< __func__ << " Update failed for ledger "
|
||||
<< std::to_string(ledgerSequence_) << ". Returning";
|
||||
log_.warn() << "Update failed for ledger " << std::to_string(ledgerSequence_) << ". Returning";
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __func__ << " Committed ledger "
|
||||
<< std::to_string(ledgerSequence_);
|
||||
log_.info() << "Committed ledger " << std::to_string(ledgerSequence_);
|
||||
lastSync_ = ledgerSequence_;
|
||||
}
|
||||
else
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< __func__ << " Skipping commit. sync interval is "
|
||||
<< std::to_string(syncInterval_) << " - last sync is "
|
||||
<< std::to_string(lastSync_) << " - ledger sequence is "
|
||||
<< std::to_string(ledgerSequence_);
|
||||
log_.info() << "Skipping commit. sync interval is " << std::to_string(syncInterval_) << " - last sync is "
|
||||
<< std::to_string(lastSync_) << " - ledger sequence is " << std::to_string(ledgerSequence_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -820,36 +794,32 @@ public:
|
||||
return doFinishWritesAsync();
|
||||
}
|
||||
void
|
||||
writeLedger(ripple::LedgerInfo const& ledgerInfo, std::string&& header)
|
||||
override;
|
||||
writeLedger(ripple::LedgerInfo const& ledgerInfo, std::string&& header) override;
|
||||
|
||||
std::optional<std::uint32_t>
|
||||
fetchLatestLedgerSequence(boost::asio::yield_context& yield) const override
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__;
|
||||
log_.trace() << "called";
|
||||
CassandraStatement statement{selectLatestLedger_};
|
||||
CassandraResult result = executeAsyncRead(statement, yield);
|
||||
if (!result.hasResult())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "CassandraBackend::fetchLatestLedgerSequence - no rows";
|
||||
log_.error() << "CassandraBackend::fetchLatestLedgerSequence - no rows";
|
||||
return {};
|
||||
}
|
||||
return result.getUInt32();
|
||||
}
|
||||
|
||||
std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerBySequence(
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const override
|
||||
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__;
|
||||
log_.trace() << "called";
|
||||
CassandraStatement statement{selectLedgerBySeq_};
|
||||
statement.bindNextInt(sequence);
|
||||
CassandraResult result = executeAsyncRead(statement, yield);
|
||||
if (!result)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
|
||||
log_.error() << "No rows";
|
||||
return {};
|
||||
}
|
||||
std::vector<unsigned char> header = result.getBytes();
|
||||
@@ -857,9 +827,7 @@ public:
|
||||
}
|
||||
|
||||
std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerByHash(
|
||||
ripple::uint256 const& hash,
|
||||
boost::asio::yield_context& yield) const override
|
||||
fetchLedgerByHash(ripple::uint256 const& hash, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
CassandraStatement statement{selectLedgerByHash_};
|
||||
|
||||
@@ -869,7 +837,7 @@ public:
|
||||
|
||||
if (!result.hasResult())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << __func__ << " - no rows returned";
|
||||
log_.debug() << "No rows returned";
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -882,20 +850,15 @@ public:
|
||||
hardFetchLedgerRange(boost::asio::yield_context& yield) const override;
|
||||
|
||||
std::vector<TransactionAndMetadata>
|
||||
fetchAllTransactionsInLedger(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const override;
|
||||
|
||||
std::vector<ripple::uint256>
|
||||
fetchAllTransactionHashesInLedger(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
fetchAllTransactionHashesInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
|
||||
std::optional<NFT>
|
||||
fetchNFT(
|
||||
ripple::uint256 const& tokenID,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
|
||||
TransactionsAndCursor
|
||||
fetchNFTTransactions(
|
||||
@@ -908,64 +871,31 @@ public:
|
||||
// Synchronously fetch the object with key key, as of ledger with sequence
|
||||
// sequence
|
||||
std::optional<Blob>
|
||||
doFetchLedgerObject(
|
||||
ripple::uint256 const& key,
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
|
||||
std::optional<int64_t>
|
||||
getToken(void const* key, boost::asio::yield_context& yield) const
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "Fetching from cassandra";
|
||||
CassandraStatement statement{getToken_};
|
||||
statement.bindNextBytes(key, 32);
|
||||
|
||||
CassandraResult result = executeAsyncRead(statement, yield);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
|
||||
return {};
|
||||
}
|
||||
int64_t token = result.getInt64();
|
||||
if (token == INT64_MAX)
|
||||
return {};
|
||||
else
|
||||
return token + 1;
|
||||
}
|
||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
|
||||
std::optional<TransactionAndMetadata>
|
||||
fetchTransaction(
|
||||
ripple::uint256 const& hash,
|
||||
boost::asio::yield_context& yield) const override
|
||||
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__;
|
||||
log_.trace() << "called";
|
||||
CassandraStatement statement{selectTransaction_};
|
||||
statement.bindNextBytes(hash);
|
||||
CassandraResult result = executeAsyncRead(statement, yield);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
|
||||
log_.error() << "No rows";
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
{result.getBytes(),
|
||||
result.getBytes(),
|
||||
result.getUInt32(),
|
||||
result.getUInt32()}};
|
||||
return {{result.getBytes(), result.getBytes(), result.getUInt32(), result.getUInt32()}};
|
||||
}
|
||||
|
||||
std::optional<ripple::uint256>
|
||||
doFetchSuccessorKey(
|
||||
ripple::uint256 key,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
|
||||
std::vector<TransactionAndMetadata>
|
||||
fetchTransactions(
|
||||
std::vector<ripple::uint256> const& hashes,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context& yield) const override;
|
||||
|
||||
std::vector<Blob>
|
||||
doFetchLedgerObjects(
|
||||
@@ -974,25 +904,16 @@ public:
|
||||
boost::asio::yield_context& yield) const override;
|
||||
|
||||
std::vector<LedgerObject>
|
||||
fetchLedgerDiff(
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const override;
|
||||
|
||||
void
|
||||
doWriteLedgerObject(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& blob) override;
|
||||
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) override;
|
||||
|
||||
void
|
||||
writeSuccessor(
|
||||
std::string&& key,
|
||||
std::uint32_t const seq,
|
||||
std::string&& successor) override;
|
||||
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override;
|
||||
|
||||
void
|
||||
writeAccountTransactions(
|
||||
std::vector<AccountTransactionsData>&& data) override;
|
||||
writeAccountTransactions(std::vector<AccountTransactionsData>&& data) override;
|
||||
|
||||
void
|
||||
writeNFTTransactions(std::vector<NFTTransactionsData>&& data) override;
|
||||
@@ -1022,9 +943,7 @@ public:
|
||||
}
|
||||
|
||||
bool
|
||||
doOnlineDelete(
|
||||
std::uint32_t const numLedgersToKeep,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
doOnlineDelete(std::uint32_t const numLedgersToKeep, boost::asio::yield_context& yield) const override;
|
||||
|
||||
bool
|
||||
isTooBusy() const override;
|
||||
@@ -1036,10 +955,8 @@ public:
|
||||
std::unique_lock<std::mutex> lck(throttleMutex_);
|
||||
if (!canAddRequest())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< __func__ << " : "
|
||||
<< "Max outstanding requests reached. "
|
||||
<< "Waiting for other requests to finish";
|
||||
log_.debug() << "Max outstanding requests reached. "
|
||||
<< "Waiting for other requests to finish";
|
||||
throttleCv_.wait(lck, [this]() { return canAddRequest(); });
|
||||
}
|
||||
}
|
||||
@@ -1091,26 +1008,18 @@ public:
|
||||
|
||||
template <class T, class S>
|
||||
void
|
||||
executeAsyncHelper(
|
||||
CassandraStatement const& statement,
|
||||
T callback,
|
||||
S& callbackData) const
|
||||
executeAsyncHelper(CassandraStatement const& statement, T callback, S& callbackData) const
|
||||
{
|
||||
CassFuture* fut = cass_session_execute(session_.get(), statement.get());
|
||||
|
||||
cass_future_set_callback(
|
||||
fut, callback, static_cast<void*>(&callbackData));
|
||||
cass_future_set_callback(fut, callback, static_cast<void*>(&callbackData));
|
||||
|
||||
cass_future_free(fut);
|
||||
}
|
||||
|
||||
template <class T, class S>
|
||||
void
|
||||
executeAsyncWrite(
|
||||
CassandraStatement const& statement,
|
||||
T callback,
|
||||
S& callbackData,
|
||||
bool isRetry) const
|
||||
executeAsyncWrite(CassandraStatement const& statement, T callback, S& callbackData, bool isRetry) const
|
||||
{
|
||||
if (!isRetry)
|
||||
incrementOutstandingRequestCount();
|
||||
@@ -1119,10 +1028,7 @@ public:
|
||||
|
||||
template <class T, class S>
|
||||
void
|
||||
executeAsyncRead(
|
||||
CassandraStatement const& statement,
|
||||
T callback,
|
||||
S& callbackData) const
|
||||
executeAsyncRead(CassandraStatement const& statement, T callback, S& callbackData) const
|
||||
{
|
||||
executeAsyncHelper(statement, callback, callbackData);
|
||||
}
|
||||
@@ -1142,7 +1048,7 @@ public:
|
||||
ss << "Cassandra sync write error";
|
||||
ss << ", retrying";
|
||||
ss << ": " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(warning) << ss.str();
|
||||
log_.warn() << ss.str();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
} while (rc != CASS_OK);
|
||||
@@ -1166,7 +1072,7 @@ public:
|
||||
ss << "Cassandra sync update error";
|
||||
ss << ", retrying";
|
||||
ss << ": " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(warning) << ss.str();
|
||||
log_.warn() << ss.str();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
} while (rc != CASS_OK);
|
||||
@@ -1176,7 +1082,7 @@ public:
|
||||
CassRow const* row = cass_result_first_row(res);
|
||||
if (!row)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "executeSyncUpdate - no rows";
|
||||
log_.error() << "executeSyncUpdate - no rows";
|
||||
cass_result_free(res);
|
||||
return false;
|
||||
}
|
||||
@@ -1185,16 +1091,13 @@ public:
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
cass_result_free(res);
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "executeSyncUpdate - error getting result " << rc << ", "
|
||||
<< cass_error_desc(rc);
|
||||
log_.error() << "executeSyncUpdate - error getting result " << rc << ", " << cass_error_desc(rc);
|
||||
return false;
|
||||
}
|
||||
cass_result_free(res);
|
||||
if (success != cass_true && timedOut)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< __func__ << " Update failed, but timedOut is true";
|
||||
log_.warn() << "Update failed, but timedOut is true";
|
||||
// if there was a timeout, the update may have succeeded in the
|
||||
// background on the first attempt. To determine if this happened,
|
||||
// we query the range from the db, making sure the range is what
|
||||
@@ -1209,13 +1112,10 @@ public:
|
||||
}
|
||||
|
||||
CassandraResult
|
||||
executeAsyncRead(
|
||||
CassandraStatement const& statement,
|
||||
boost::asio::yield_context& yield) const
|
||||
executeAsyncRead(CassandraStatement const& statement, boost::asio::yield_context& yield) const
|
||||
{
|
||||
using result = boost::asio::async_result<
|
||||
boost::asio::yield_context,
|
||||
void(boost::system::error_code, CassError)>;
|
||||
using result =
|
||||
boost::asio::async_result<boost::asio::yield_context, void(boost::system::error_code, CassError)>;
|
||||
|
||||
CassFuture* fut;
|
||||
CassError rc;
|
||||
@@ -1230,15 +1130,14 @@ public:
|
||||
|
||||
if (ec)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "Cannot read async cass_future_error_code";
|
||||
log_.error() << "Cannot read async cass_future_error_code";
|
||||
}
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Cassandra executeAsyncRead error";
|
||||
ss << ": " << cass_error_desc(rc);
|
||||
BOOST_LOG_TRIVIAL(error) << ss.str();
|
||||
log_.error() << ss.str();
|
||||
}
|
||||
if (isTimeout(rc))
|
||||
{
|
||||
@@ -1261,4 +1160,3 @@ public:
|
||||
};
|
||||
|
||||
} // namespace Backend
|
||||
#endif
|
||||
|
||||
851
src/backend/CassandraBackendNew.h
Normal file
851
src/backend/CassandraBackendNew.h
Normal file
@@ -0,0 +1,851 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <backend/cassandra/Concepts.h>
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/Schema.h>
|
||||
#include <backend/cassandra/SettingsProvider.h>
|
||||
#include <backend/cassandra/impl/ExecutionStrategy.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Profiler.h>
|
||||
|
||||
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
/**
|
||||
* @brief Implements @ref BackendInterface for Cassandra/Scylladb
|
||||
*
|
||||
* Note: this is a safer and more correct rewrite of the original implementation
|
||||
* of the backend. We deliberately did not change the interface for now so that
|
||||
* other parts such as ETL do not have to change at all.
|
||||
* Eventually we should change the interface so that it does not have to know
|
||||
* about yield_context.
|
||||
*/
|
||||
template <SomeSettingsProvider SettingsProviderType, SomeExecutionStrategy ExecutionStrategy>
|
||||
class BasicCassandraBackend : public BackendInterface
|
||||
{
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
SettingsProviderType settingsProvider_;
|
||||
Schema<SettingsProviderType> schema_;
|
||||
Handle handle_;
|
||||
|
||||
// have to be mutable because BackendInterface constness :(
|
||||
mutable ExecutionStrategy executor_;
|
||||
|
||||
std::atomic_uint32_t ledgerSequence_ = 0u;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new cassandra/scylla backend instance.
|
||||
*
|
||||
* @param settingsProvider
|
||||
*/
|
||||
BasicCassandraBackend(SettingsProviderType settingsProvider)
|
||||
: settingsProvider_{std::move(settingsProvider)}
|
||||
, schema_{settingsProvider_}
|
||||
, handle_{settingsProvider_.getSettings()}
|
||||
, executor_{settingsProvider_.getSettings(), handle_}
|
||||
{
|
||||
if (auto const res = handle_.connect(); not res)
|
||||
throw std::runtime_error("Could not connect to Cassandra: " + res.error());
|
||||
|
||||
if (auto const res = handle_.execute(schema_.createKeyspace); not res)
|
||||
{
|
||||
// on datastax, creation of keyspaces can be configured to only be done thru the admin interface.
|
||||
// this does not mean that the keyspace does not already exist tho.
|
||||
if (res.error().code() != CASS_ERROR_SERVER_UNAUTHORIZED)
|
||||
throw std::runtime_error("Could not create keyspace: " + res.error());
|
||||
}
|
||||
|
||||
if (auto const res = handle_.executeEach(schema_.createSchema); not res)
|
||||
throw std::runtime_error("Could not create schema: " + res.error());
|
||||
|
||||
schema_.prepareStatements(handle_);
|
||||
log_.info() << "Created (revamped) CassandraBackend";
|
||||
}
|
||||
|
||||
/*! Not used in this implementation */
|
||||
void
|
||||
open([[maybe_unused]] bool readOnly) override
|
||||
{
|
||||
}
|
||||
|
||||
/*! Not used in this implementation */
|
||||
void
|
||||
close() override
|
||||
{
|
||||
}
|
||||
|
||||
TransactionsAndCursor
|
||||
fetchAccountTransactions(
|
||||
ripple::AccountID const& account,
|
||||
std::uint32_t const limit,
|
||||
bool forward,
|
||||
std::optional<TransactionsCursor> const& cursorIn,
|
||||
boost::asio::yield_context& yield) const override
|
||||
{
|
||||
auto rng = fetchLedgerRange();
|
||||
if (!rng)
|
||||
return {{}, {}};
|
||||
|
||||
Statement statement = [this, forward, &account]() {
|
||||
if (forward)
|
||||
return schema_->selectAccountTxForward.bind(account);
|
||||
else
|
||||
return schema_->selectAccountTx.bind(account);
|
||||
}();
|
||||
|
||||
auto cursor = cursorIn;
|
||||
if (cursor)
|
||||
{
|
||||
statement.bindAt(1, cursor->asTuple());
|
||||
log_.debug() << "account = " << ripple::strHex(account) << " tuple = " << cursor->ledgerSequence
|
||||
<< cursor->transactionIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const seq = forward ? rng->minSequence : rng->maxSequence;
|
||||
auto const placeHolder = forward ? 0u : std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
statement.bindAt(1, std::make_tuple(placeHolder, placeHolder));
|
||||
log_.debug() << "account = " << ripple::strHex(account) << " idx = " << seq << " tuple = " << placeHolder;
|
||||
}
|
||||
|
||||
// FIXME: Limit is a hack to support uint32_t properly for the time
|
||||
// being. Should be removed later and schema updated to use proper
|
||||
// types.
|
||||
statement.bindAt(2, Limit{limit});
|
||||
auto const res = executor_.read(yield, statement);
|
||||
auto const& results = res.value();
|
||||
if (not results.hasRows())
|
||||
{
|
||||
log_.debug() << "No rows returned";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> hashes = {};
|
||||
auto numRows = results.numRows();
|
||||
log_.info() << "num_rows = " << numRows;
|
||||
|
||||
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results))
|
||||
{
|
||||
hashes.push_back(hash);
|
||||
if (--numRows == 0)
|
||||
{
|
||||
log_.debug() << "Setting cursor";
|
||||
cursor = data;
|
||||
|
||||
// forward queries by ledger/tx sequence `>=`
|
||||
// so we have to advance the index by one
|
||||
if (forward)
|
||||
++cursor->transactionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
auto const txns = fetchTransactions(hashes, yield);
|
||||
log_.debug() << "Txns = " << txns.size();
|
||||
|
||||
if (txns.size() == limit)
|
||||
{
|
||||
log_.debug() << "Returning cursor";
|
||||
return {txns, cursor};
|
||||
}
|
||||
|
||||
return {txns, {}};
|
||||
}
|
||||
|
||||
bool
|
||||
doFinishWrites() override
|
||||
{
|
||||
// wait for other threads to finish their writes
|
||||
executor_.sync();
|
||||
|
||||
if (!range)
|
||||
{
|
||||
executor_.writeSync(schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_);
|
||||
}
|
||||
|
||||
if (not executeSyncUpdate(schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1)))
|
||||
{
|
||||
log_.warn() << "Update failed for ledger " << ledgerSequence_;
|
||||
return false;
|
||||
}
|
||||
|
||||
log_.info() << "Committed ledger " << ledgerSequence_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
writeLedger(ripple::LedgerInfo const& ledgerInfo, std::string&& header) override
|
||||
{
|
||||
executor_.write(schema_->insertLedgerHeader, ledgerInfo.seq, std::move(header));
|
||||
|
||||
executor_.write(schema_->insertLedgerHash, ledgerInfo.hash, ledgerInfo.seq);
|
||||
|
||||
ledgerSequence_ = ledgerInfo.seq;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t>
|
||||
fetchLatestLedgerSequence(boost::asio::yield_context& yield) const override
|
||||
{
|
||||
if (auto const res = executor_.read(yield, schema_->selectLatestLedger); res)
|
||||
{
|
||||
if (auto const& result = res.value(); result)
|
||||
{
|
||||
if (auto const maybeValue = result.template get<uint32_t>(); maybeValue)
|
||||
return maybeValue;
|
||||
|
||||
log_.error() << "Could not fetch latest ledger - no rows";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
log_.error() << "Could not fetch latest ledger - no result";
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch latest ledger: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call for seq " << sequence;
|
||||
|
||||
auto const res = executor_.read(yield, schema_->selectLedgerBySeq, sequence);
|
||||
if (res)
|
||||
{
|
||||
if (auto const& result = res.value(); result)
|
||||
{
|
||||
if (auto const maybeValue = result.template get<std::vector<unsigned char>>(); maybeValue)
|
||||
{
|
||||
return deserializeHeader(ripple::makeSlice(*maybeValue));
|
||||
}
|
||||
|
||||
log_.error() << "Could not fetch ledger by sequence - no rows";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
log_.error() << "Could not fetch ledger by sequence - no result";
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch ledger by sequence: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<ripple::LedgerInfo>
|
||||
fetchLedgerByHash(ripple::uint256 const& hash, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (auto const res = executor_.read(yield, schema_->selectLedgerByHash, hash); res)
|
||||
{
|
||||
if (auto const& result = res.value(); result)
|
||||
{
|
||||
if (auto const maybeValue = result.template get<uint32_t>(); maybeValue)
|
||||
return fetchLedgerBySequence(*maybeValue, yield);
|
||||
|
||||
log_.error() << "Could not fetch ledger by hash - no rows";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
log_.error() << "Could not fetch ledger by hash - no result";
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch ledger by hash: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<LedgerRange>
|
||||
hardFetchLedgerRange(boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (auto const res = executor_.read(yield, schema_->selectLedgerRange); res)
|
||||
{
|
||||
auto const& results = res.value();
|
||||
if (not results.hasRows())
|
||||
{
|
||||
log_.debug() << "Could not fetch ledger range - no rows";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// TODO: this is probably a good place to use user type in
|
||||
// cassandra instead of having two rows with bool flag. or maybe at
|
||||
// least use tuple<int, int>?
|
||||
LedgerRange range;
|
||||
std::size_t idx = 0;
|
||||
for (auto [seq] : extract<uint32_t>(results))
|
||||
{
|
||||
if (idx == 0)
|
||||
range.maxSequence = range.minSequence = seq;
|
||||
else if (idx == 1)
|
||||
range.maxSequence = seq;
|
||||
|
||||
++idx;
|
||||
}
|
||||
|
||||
if (range.minSequence > range.maxSequence)
|
||||
std::swap(range.minSequence, range.maxSequence);
|
||||
|
||||
log_.debug() << "After hardFetchLedgerRange range is " << range.minSequence << ":" << range.maxSequence;
|
||||
return range;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch ledger range: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<TransactionAndMetadata>
|
||||
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
auto hashes = fetchAllTransactionHashesInLedger(ledgerSequence, yield);
|
||||
return fetchTransactions(hashes, yield);
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256>
|
||||
fetchAllTransactionHashesInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
auto start = std::chrono::system_clock::now();
|
||||
auto const res = executor_.read(yield, schema_->selectAllTransactionHashesInLedger, ledgerSequence);
|
||||
|
||||
if (not res)
|
||||
{
|
||||
log_.error() << "Could not fetch all transaction hashes: " << res.error();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto const& result = res.value();
|
||||
if (not result.hasRows())
|
||||
{
|
||||
log_.error() << "Could not fetch all transaction hashes - no rows; ledger = "
|
||||
<< std::to_string(ledgerSequence);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> hashes;
|
||||
for (auto [hash] : extract<ripple::uint256>(result))
|
||||
hashes.push_back(std::move(hash));
|
||||
|
||||
auto end = std::chrono::system_clock::now();
|
||||
log_.debug() << "Fetched " << hashes.size() << " transaction hashes from Cassandra in "
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " milliseconds";
|
||||
|
||||
return hashes;
|
||||
}
|
||||
|
||||
std::optional<NFT>
|
||||
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
auto const res = executor_.read(yield, schema_->selectNFT, tokenID, ledgerSequence);
|
||||
if (not res)
|
||||
return std::nullopt;
|
||||
|
||||
if (auto const maybeRow = res->template get<uint32_t, ripple::AccountID, bool>(); maybeRow)
|
||||
{
|
||||
auto [seq, owner, isBurned] = *maybeRow;
|
||||
auto result = std::make_optional<NFT>(tokenID, seq, owner, isBurned);
|
||||
|
||||
// now fetch URI. Usually we will have the URI even for burned NFTs,
|
||||
// but if the first ledger on this clio included NFTokenBurn
|
||||
// transactions we will not have the URIs for any of those tokens.
|
||||
// In any other case not having the URI indicates something went
|
||||
// wrong with our data.
|
||||
//
|
||||
// TODO - in the future would be great for any handlers that use
|
||||
// this could inject a warning in this case (the case of not having
|
||||
// a URI because it was burned in the first ledger) to indicate that
|
||||
// even though we are returning a blank URI, the NFT might have had
|
||||
// one.
|
||||
auto uriRes = executor_.read(yield, schema_->selectNFTURI, tokenID, ledgerSequence);
|
||||
if (uriRes)
|
||||
{
|
||||
if (auto const maybeUri = uriRes->template get<ripple::Blob>(); maybeUri)
|
||||
result->uri = *maybeUri;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
log_.error() << "Could not fetch NFT - no rows";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TransactionsAndCursor
|
||||
fetchNFTTransactions(
|
||||
ripple::uint256 const& tokenID,
|
||||
std::uint32_t const limit,
|
||||
bool const forward,
|
||||
std::optional<TransactionsCursor> const& cursorIn,
|
||||
boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
auto rng = fetchLedgerRange();
|
||||
if (!rng)
|
||||
return {{}, {}};
|
||||
|
||||
Statement statement = [this, forward, &tokenID]() {
|
||||
if (forward)
|
||||
return schema_->selectNFTTxForward.bind(tokenID);
|
||||
else
|
||||
return schema_->selectNFTTx.bind(tokenID);
|
||||
}();
|
||||
|
||||
auto cursor = cursorIn;
|
||||
if (cursor)
|
||||
{
|
||||
statement.bindAt(1, cursor->asTuple());
|
||||
log_.debug() << "token_id = " << ripple::strHex(tokenID) << " tuple = " << cursor->ledgerSequence
|
||||
<< cursor->transactionIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const seq = forward ? rng->minSequence : rng->maxSequence;
|
||||
auto const placeHolder = forward ? 0 : std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
statement.bindAt(1, std::make_tuple(placeHolder, placeHolder));
|
||||
log_.debug() << "token_id = " << ripple::strHex(tokenID) << " idx = " << seq << " tuple = " << placeHolder;
|
||||
}
|
||||
|
||||
statement.bindAt(2, Limit{limit});
|
||||
|
||||
auto const res = executor_.read(yield, statement);
|
||||
auto const& results = res.value();
|
||||
if (not results.hasRows())
|
||||
{
|
||||
log_.debug() << "No rows returned";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> hashes = {};
|
||||
auto numRows = results.numRows();
|
||||
log_.info() << "num_rows = " << numRows;
|
||||
|
||||
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results))
|
||||
{
|
||||
hashes.push_back(hash);
|
||||
if (--numRows == 0)
|
||||
{
|
||||
log_.debug() << "Setting cursor";
|
||||
cursor = data;
|
||||
|
||||
// forward queries by ledger/tx sequence `>=`
|
||||
// so we have to advance the index by one
|
||||
if (forward)
|
||||
++cursor->transactionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
auto const txns = fetchTransactions(hashes, yield);
|
||||
log_.debug() << "NFT Txns = " << txns.size();
|
||||
|
||||
if (txns.size() == limit)
|
||||
{
|
||||
log_.debug() << "Returning cursor";
|
||||
return {txns, cursor};
|
||||
}
|
||||
|
||||
return {txns, {}};
|
||||
}
|
||||
|
||||
std::optional<Blob>
|
||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context& yield)
|
||||
const override
|
||||
{
|
||||
log_.debug() << "Fetching ledger object for seq " << sequence << ", key = " << ripple::to_string(key);
|
||||
if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res)
|
||||
{
|
||||
if (auto const result = res->template get<Blob>(); result)
|
||||
{
|
||||
if (result->size())
|
||||
return *result;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.debug() << "Could not fetch ledger object - no rows";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch ledger object: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<TransactionAndMetadata>
|
||||
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (auto const res = executor_.read(yield, schema_->selectTransaction, hash); res)
|
||||
{
|
||||
if (auto const maybeValue = res->template get<Blob, Blob, uint32_t, uint32_t>(); maybeValue)
|
||||
{
|
||||
auto [transaction, meta, seq, date] = *maybeValue;
|
||||
return std::make_optional<TransactionAndMetadata>(transaction, meta, seq, date);
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.debug() << "Could not fetch transaction - no rows";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch transaction: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<ripple::uint256>
|
||||
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield)
|
||||
const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (auto const res = executor_.read(yield, schema_->selectSuccessor, key, ledgerSequence); res)
|
||||
{
|
||||
if (auto const result = res->template get<ripple::uint256>(); result)
|
||||
{
|
||||
if (*result == lastKey)
|
||||
return std::nullopt;
|
||||
return *result;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.debug() << "Could not fetch successor - no rows";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Could not fetch successor: " << res.error();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<TransactionAndMetadata>
|
||||
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (hashes.size() == 0)
|
||||
return {};
|
||||
|
||||
auto const numHashes = hashes.size();
|
||||
std::vector<TransactionAndMetadata> results;
|
||||
results.reserve(numHashes);
|
||||
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(numHashes);
|
||||
|
||||
auto const timeDiff = util::timed([this, &yield, &results, &hashes, &statements]() {
|
||||
// TODO: seems like a job for "hash IN (list of hashes)" instead?
|
||||
std::transform(
|
||||
std::cbegin(hashes), std::cend(hashes), std::back_inserter(statements), [this](auto const& hash) {
|
||||
return schema_->selectTransaction.bind(hash);
|
||||
});
|
||||
|
||||
auto const entries = executor_.readEach(yield, statements);
|
||||
std::transform(
|
||||
std::cbegin(entries),
|
||||
std::cend(entries),
|
||||
std::back_inserter(results),
|
||||
[](auto const& res) -> TransactionAndMetadata {
|
||||
if (auto const maybeRow = res.template get<Blob, Blob, uint32_t, uint32_t>(); maybeRow)
|
||||
return *maybeRow;
|
||||
else
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
assert(numHashes == results.size());
|
||||
log_.debug() << "Fetched " << numHashes << " transactions from Cassandra in " << timeDiff << " milliseconds";
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<Blob>
|
||||
doFetchLedgerObjects(
|
||||
std::vector<ripple::uint256> const& keys,
|
||||
std::uint32_t const sequence,
|
||||
boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
if (keys.size() == 0)
|
||||
return {};
|
||||
|
||||
auto const numKeys = keys.size();
|
||||
log_.trace() << "Fetching " << numKeys << " objects";
|
||||
|
||||
std::vector<Blob> results;
|
||||
results.reserve(numKeys);
|
||||
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(numKeys);
|
||||
|
||||
// TODO: seems like a job for "key IN (list of keys)" instead?
|
||||
std::transform(
|
||||
std::cbegin(keys), std::cend(keys), std::back_inserter(statements), [this, &sequence](auto const& key) {
|
||||
return schema_->selectObject.bind(key, sequence);
|
||||
});
|
||||
|
||||
auto const entries = executor_.readEach(yield, statements);
|
||||
std::transform(
|
||||
std::cbegin(entries), std::cend(entries), std::back_inserter(results), [](auto const& res) -> Blob {
|
||||
if (auto const maybeValue = res.template get<Blob>(); maybeValue)
|
||||
return *maybeValue;
|
||||
else
|
||||
return {};
|
||||
});
|
||||
|
||||
log_.trace() << "Fetched " << numKeys << " objects";
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<LedgerObject>
|
||||
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
|
||||
auto const [keys, timeDiff] = util::timed([this, &ledgerSequence, &yield]() -> std::vector<ripple::uint256> {
|
||||
auto const res = executor_.read(yield, schema_->selectDiff, ledgerSequence);
|
||||
if (not res)
|
||||
{
|
||||
log_.error() << "Could not fetch ledger diff: " << res.error() << "; ledger = " << ledgerSequence;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto const& results = res.value();
|
||||
if (not results)
|
||||
{
|
||||
log_.error() << "Could not fetch ledger diff - no rows; ledger = " << ledgerSequence;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> keys;
|
||||
for (auto [key] : extract<ripple::uint256>(results))
|
||||
keys.push_back(key);
|
||||
|
||||
return keys;
|
||||
});
|
||||
|
||||
// one of the above errors must have happened
|
||||
if (keys.empty())
|
||||
return {};
|
||||
|
||||
log_.debug() << "Fetched " << keys.size() << " diff hashes from Cassandra in " << timeDiff << " milliseconds";
|
||||
|
||||
auto const objs = fetchLedgerObjects(keys, ledgerSequence, yield);
|
||||
std::vector<LedgerObject> results;
|
||||
results.reserve(keys.size());
|
||||
|
||||
std::transform(
|
||||
std::cbegin(keys),
|
||||
std::cend(keys),
|
||||
std::cbegin(objs),
|
||||
std::back_inserter(results),
|
||||
[](auto const& key, auto const& obj) {
|
||||
return LedgerObject{key, obj};
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void
|
||||
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) override
|
||||
{
|
||||
log_.trace() << " Writing ledger object " << key.size() << ":" << seq << " [" << blob.size() << " bytes]";
|
||||
|
||||
if (range)
|
||||
executor_.write(schema_->insertDiff, seq, key);
|
||||
|
||||
executor_.write(schema_->insertObject, std::move(key), seq, std::move(blob));
|
||||
}
|
||||
|
||||
void
|
||||
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override
|
||||
{
|
||||
log_.trace() << "Writing successor. key = " << key.size() << " bytes. "
|
||||
<< " seq = " << std::to_string(seq) << " successor = " << successor.size() << " bytes.";
|
||||
assert(key.size() != 0);
|
||||
assert(successor.size() != 0);
|
||||
|
||||
executor_.write(schema_->insertSuccessor, std::move(key), seq, std::move(successor));
|
||||
}
|
||||
|
||||
void
|
||||
writeAccountTransactions(std::vector<AccountTransactionsData>&& data) override
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(data.size() * 10); // assume 10 transactions avg
|
||||
|
||||
for (auto& record : data)
|
||||
{
|
||||
std::transform(
|
||||
std::begin(record.accounts),
|
||||
std::end(record.accounts),
|
||||
std::back_inserter(statements),
|
||||
[this, &record](auto&& account) {
|
||||
return schema_->insertAccountTx.bind(
|
||||
std::move(account),
|
||||
std::make_tuple(record.ledgerSequence, record.transactionIndex),
|
||||
record.txHash);
|
||||
});
|
||||
}
|
||||
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
writeNFTTransactions(std::vector<NFTTransactionsData>&& data) override
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(data.size());
|
||||
|
||||
std::transform(std::cbegin(data), std::cend(data), std::back_inserter(statements), [this](auto const& record) {
|
||||
return schema_->insertNFTTx.bind(
|
||||
record.tokenID, std::make_tuple(record.ledgerSequence, record.transactionIndex), record.txHash);
|
||||
});
|
||||
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
writeTransaction(
|
||||
std::string&& hash,
|
||||
std::uint32_t const seq,
|
||||
std::uint32_t const date,
|
||||
std::string&& transaction,
|
||||
std::string&& metadata) override
|
||||
{
|
||||
log_.trace() << "Writing txn to cassandra";
|
||||
|
||||
executor_.write(schema_->insertLedgerTransaction, seq, hash);
|
||||
executor_.write(
|
||||
schema_->insertTransaction, std::move(hash), seq, date, std::move(transaction), std::move(metadata));
|
||||
}
|
||||
|
||||
void
|
||||
writeNFTs(std::vector<NFTsData>&& data) override
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(data.size() * 3);
|
||||
|
||||
for (NFTsData const& record : data)
|
||||
{
|
||||
statements.push_back(
|
||||
schema_->insertNFT.bind(record.tokenID, record.ledgerSequence, record.owner, record.isBurned));
|
||||
|
||||
// If `uri` is set (and it can be set to an empty uri), we know this
|
||||
// is a net-new NFT. That is, this NFT has not been seen before by
|
||||
// us _OR_ it is in the extreme edge case of a re-minted NFT ID with
|
||||
// the same NFT ID as an already-burned token. In this case, we need
|
||||
// to record the URI and link to the issuer_nf_tokens table.
|
||||
if (record.uri)
|
||||
{
|
||||
statements.push_back(schema_->insertIssuerNFT.bind(
|
||||
ripple::nft::getIssuer(record.tokenID),
|
||||
static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)),
|
||||
record.tokenID));
|
||||
statements.push_back(
|
||||
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value()));
|
||||
}
|
||||
}
|
||||
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
startWrites() const override
|
||||
{
|
||||
// Note: no-op in original implementation too.
|
||||
// probably was used in PG to start a transaction or smth.
|
||||
}
|
||||
|
||||
/*! Unused in this implementation */
|
||||
bool
|
||||
doOnlineDelete(std::uint32_t const numLedgersToKeep, boost::asio::yield_context& yield) const override
|
||||
{
|
||||
log_.trace() << __func__ << " call";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
isTooBusy() const override
|
||||
{
|
||||
return executor_.isTooBusy();
|
||||
}
|
||||
|
||||
private:
|
||||
bool
|
||||
executeSyncUpdate(Statement statement)
|
||||
{
|
||||
auto const res = executor_.writeSync(statement);
|
||||
auto maybeSuccess = res->template get<bool>();
|
||||
if (not maybeSuccess)
|
||||
{
|
||||
log_.error() << "executeSyncUpdate - error getting result - no row";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (not maybeSuccess.value())
|
||||
{
|
||||
log_.warn() << "Update failed. Checking if DB state is what we expect";
|
||||
|
||||
// error may indicate that another writer wrote something.
|
||||
// in this case let's just compare the current state of things
|
||||
// against what we were trying to write in the first place and
|
||||
// use that as the source of truth for the result.
|
||||
auto rng = hardFetchLedgerRangeNoThrow();
|
||||
return rng && rng->maxSequence == ledgerSequence_;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
using CassandraBackend = BasicCassandraBackend<SettingsProvider, detail::DefaultExecutionStrategy<>>;
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef CLIO_BACKEND_DBHELPERS_H_INCLUDED
|
||||
#define CLIO_BACKEND_DBHELPERS_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
@@ -21,10 +39,7 @@ struct AccountTransactionsData
|
||||
std::uint32_t transactionIndex;
|
||||
ripple::uint256 txHash;
|
||||
|
||||
AccountTransactionsData(
|
||||
ripple::TxMeta& meta,
|
||||
ripple::uint256 const& txHash,
|
||||
beast::Journal& j)
|
||||
AccountTransactionsData(ripple::TxMeta& meta, ripple::uint256 const& txHash, beast::Journal& j)
|
||||
: accounts(meta.getAffectedAccounts())
|
||||
, ledgerSequence(meta.getLgrSeq())
|
||||
, transactionIndex(meta.getIndex())
|
||||
@@ -44,14 +59,8 @@ struct NFTTransactionsData
|
||||
std::uint32_t transactionIndex;
|
||||
ripple::uint256 txHash;
|
||||
|
||||
NFTTransactionsData(
|
||||
ripple::uint256 const& tokenID,
|
||||
ripple::TxMeta const& meta,
|
||||
ripple::uint256 const& txHash)
|
||||
: tokenID(tokenID)
|
||||
, ledgerSequence(meta.getLgrSeq())
|
||||
, transactionIndex(meta.getIndex())
|
||||
, txHash(txHash)
|
||||
NFTTransactionsData(ripple::uint256 const& tokenID, ripple::TxMeta const& meta, ripple::uint256 const& txHash)
|
||||
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), txHash(txHash)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -67,16 +76,38 @@ struct NFTsData
|
||||
// final state of an NFT per ledger. Since we pull this from transactions
|
||||
// we keep track of which tx index created this so we can de-duplicate, as
|
||||
// it is possible for one ledger to have multiple txs that change the
|
||||
// state of the same NFT.
|
||||
std::uint32_t transactionIndex;
|
||||
// state of the same NFT. This field is not applicable when we are loading
|
||||
// initial NFT state via ledger objects, since we do not have to tiebreak
|
||||
// NFT state for a given ledger in that case.
|
||||
std::optional<std::uint32_t> transactionIndex;
|
||||
ripple::AccountID owner;
|
||||
bool isBurned;
|
||||
// We only set the uri if this is a mint tx, or if we are
|
||||
// loading initial state from NFTokenPage objects. In other words,
|
||||
// uri should only be set if the etl process believes this NFT hasn't
|
||||
// been seen before in our local database. We do this so that we don't
|
||||
// write to the the nf_token_uris table every
|
||||
// time the same NFT changes hands. We also can infer if there is a URI
|
||||
// that we need to write to the issuer_nf_tokens table.
|
||||
std::optional<ripple::Blob> uri;
|
||||
bool isBurned = false;
|
||||
|
||||
// This constructor is used when parsing an NFTokenMint tx.
|
||||
// Unfortunately because of the extreme edge case of being able to
|
||||
// re-mint an NFT with the same ID, we must explicitly record a null
|
||||
// URI. For this reason, we _always_ write this field as a result of
|
||||
// this tx.
|
||||
NFTsData(
|
||||
ripple::uint256 const& tokenID,
|
||||
ripple::AccountID const& owner,
|
||||
ripple::TxMeta const& meta,
|
||||
bool isBurned)
|
||||
ripple::Blob const& uri,
|
||||
ripple::TxMeta const& meta)
|
||||
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), owner(owner), uri(uri)
|
||||
{
|
||||
}
|
||||
|
||||
// This constructor is used when parsing an NFTokenBurn or
|
||||
// NFTokenAcceptOffer tx
|
||||
NFTsData(ripple::uint256 const& tokenID, ripple::AccountID const& owner, ripple::TxMeta const& meta, bool isBurned)
|
||||
: tokenID(tokenID)
|
||||
, ledgerSequence(meta.getLgrSeq())
|
||||
, transactionIndex(meta.getIndex())
|
||||
@@ -84,6 +115,21 @@ struct NFTsData
|
||||
, isBurned(isBurned)
|
||||
{
|
||||
}
|
||||
|
||||
// This constructor is used when parsing an NFTokenPage directly from
|
||||
// ledger state.
|
||||
// Unfortunately because of the extreme edge case of being able to
|
||||
// re-mint an NFT with the same ID, we must explicitly record a null
|
||||
// URI. For this reason, we _always_ write this field as a result of
|
||||
// this tx.
|
||||
NFTsData(
|
||||
ripple::uint256 const& tokenID,
|
||||
std::uint32_t const ledgerSequence,
|
||||
ripple::AccountID const& owner,
|
||||
ripple::Blob const& uri)
|
||||
: tokenID(tokenID), ledgerSequence(ledgerSequence), owner(owner), uri(uri)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
@@ -122,8 +168,7 @@ isBookDir(T const& key, R const& object)
|
||||
if (!isDirNode(object))
|
||||
return false;
|
||||
|
||||
ripple::STLedgerEntry const sle{
|
||||
ripple::SerialIter{object.data(), object.size()}, key};
|
||||
ripple::STLedgerEntry const sle{ripple::SerialIter{object.data(), object.size()}, key};
|
||||
return !sle[~ripple::sfOwner].has_value();
|
||||
}
|
||||
|
||||
@@ -162,10 +207,8 @@ deserializeHeader(ripple::Slice data)
|
||||
info.parentHash = sit.get256();
|
||||
info.txHash = sit.get256();
|
||||
info.accountHash = sit.get256();
|
||||
info.parentCloseTime =
|
||||
ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
|
||||
info.closeTime =
|
||||
ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
|
||||
info.parentCloseTime = ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
|
||||
info.closeTime = ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
|
||||
info.closeTimeResolution = ripple::NetClock::duration{sit.get8()};
|
||||
info.closeFlags = sit.get8();
|
||||
|
||||
@@ -181,4 +224,3 @@ uint256ToString(ripple::uint256 const& uint)
|
||||
}
|
||||
|
||||
static constexpr std::uint32_t rippleEpochStart = 946684800;
|
||||
#endif
|
||||
|
||||
@@ -130,3 +130,91 @@ In each new ledger version with sequence `n`, a ledger object `v` can either be
|
||||
1. Being **created**, add two new records of `seq=n` with one being `e` pointing to `v`, and `v` pointing to `w` (Linked List insertion operation).
|
||||
2. Being **modified**, do nothing.
|
||||
3. Being **deleted**, add a record of `seq=n` with `e` pointing to `v`'s `next` value (Linked List deletion operation).
|
||||
|
||||
### NFT data model
|
||||
In `rippled` NFTs are stored in NFTokenPage ledger objects. This object is
|
||||
implemented to save ledger space and has the property that it gives us O(1)
|
||||
lookup time for an NFT, assuming we know who owns the NFT at a particular
|
||||
ledger. However, if we do not know who owns the NFT at a specific ledger
|
||||
height we have no alternative in rippled other than scanning the entire
|
||||
ledger. Because of this tradeoff, clio implements a special NFT indexing data
|
||||
structure that allows clio users to query NFTs quickly, while keeping
|
||||
rippled's space-saving optimizations.
|
||||
|
||||
#### `nf_tokens`
|
||||
```
|
||||
CREATE TABLE clio.nf_tokens (
|
||||
token_id blob, # The NFT's ID
|
||||
sequence bigint, # Sequence of ledger version
|
||||
owner blob, # The account ID of the owner of this NFT at this ledger
|
||||
is_burned boolean, # True if token was burned in this ledger
|
||||
PRIMARY KEY (token_id, sequence)
|
||||
) WITH CLUSTERING ORDER BY (sequence DESC) ...
|
||||
```
|
||||
This table indexes NFT IDs with their owner at a given ledger. So
|
||||
```
|
||||
SELECT * FROM nf_tokens
|
||||
WHERE token_id = N AND seq <= Y
|
||||
ORDER BY seq DESC LIMIT 1;
|
||||
```
|
||||
will give you the owner of token N at ledger Y and whether it was burned. If
|
||||
the token is burned, the owner field indicates the account that owned the
|
||||
token at the time it was burned; it does not indicate the person who burned
|
||||
the token, necessarily. If you need to determine who burned the token you can
|
||||
use the `nft_history` API, which will give you the NFTokenBurn transaction
|
||||
that burned this token, along with the account that submitted that
|
||||
transaction.
|
||||
|
||||
#### `issuer_nf_tokens_v2`
|
||||
```
|
||||
CREATE TABLE clio.issuer_nf_tokens_v2 (
|
||||
issuer blob, # The NFT issuer's account ID
|
||||
taxon bigint, # The NFT's token taxon
|
||||
token_id blob, # The NFT's ID
|
||||
PRIMARY KEY (issuer, taxon, token_id)
|
||||
) WITH CLUSTERING ORDER BY (taxon ASC, token_id ASC) ...
|
||||
```
|
||||
This table indexes token IDs against their issuer and issuer/taxon
|
||||
combination. This is useful for determining all the NFTs a specific account
|
||||
issued, or all the NFTs a specific account issued with a specific taxon. It is
|
||||
not useful to know all the NFTs with a given taxon while excluding issuer, since the
|
||||
meaning of a taxon is left to an issuer.
|
||||
|
||||
#### `nf_token_uris`
|
||||
```
|
||||
CREATE TABLE clio.nf_token_uris (
|
||||
token_id blob, # The NFT's ID
|
||||
sequence bigint, # Sequence of ledger version
|
||||
uri blob, # The NFT's URI
|
||||
PRIMARY KEY (token_id, sequence)
|
||||
) WITH CLUSTERING ORDER BY (sequence DESC) ...
|
||||
```
|
||||
This table is used to store an NFT's URI. Without storing this here, we would
|
||||
need to traverse the NFT owner's entire set of NFTs to find the URI, again due
|
||||
to the way that NFTs are stored in rippled. Furthermore, instead of storing
|
||||
this in the `nf_tokens` table, we store it here to save space. A given NFT
|
||||
will have only one entry in this table (see caveat below), written to this
|
||||
table as soon as clio sees the NFTokenMint transaction, or when clio loads an
|
||||
NFTokenPage from the initial ledger it downloaded. However, the `nf_tokens`
|
||||
table is written to every time an NFT changes ownership, or if it is burned.
|
||||
|
||||
Given this, why do we have to store the sequence? Unfortunately there is an
|
||||
extreme edge case where a given NFT ID can be burned, and then re-minted with
|
||||
a different URI. This is extremely unlikely, and might be fixed in a future
|
||||
version to rippled, but just in case we can handle that edge case by allowing
|
||||
a given NFT ID to have a new URI assigned in this case, without removing the
|
||||
prior URI.
|
||||
|
||||
#### `nf_token_transactions`
|
||||
```
|
||||
CREATE TABLE clio.nf_token_transactions (
|
||||
token_id blob, # The NFT's ID
|
||||
seq_idx tuple<bigint, bigint>, # Tuple of (ledger_index, transaction_index)
|
||||
hash blob, # Hash of the transaction
|
||||
PRIMARY KEY (token_id, seq_idx)
|
||||
) WITH CLUSTERING ORDER BY (seq_idx DESC) ...
|
||||
```
|
||||
This table is the NFT equivalent of `account_tx`. It's motivated by the exact
|
||||
same reasons and serves the analogous purpose here. It drives the
|
||||
`nft_history` API.
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/SimpleCache.h>
|
||||
namespace Backend {
|
||||
|
||||
@@ -9,16 +28,13 @@ SimpleCache::latestLedgerSequence() const
|
||||
}
|
||||
|
||||
void
|
||||
SimpleCache::update(
|
||||
std::vector<LedgerObject> const& objs,
|
||||
uint32_t seq,
|
||||
bool isBackground)
|
||||
SimpleCache::update(std::vector<LedgerObject> const& objs, uint32_t seq, bool isBackground)
|
||||
{
|
||||
if (disabled_)
|
||||
return;
|
||||
|
||||
{
|
||||
std::unique_lock lck{mtx_};
|
||||
std::scoped_lock lck{mtx_};
|
||||
if (seq > latestSeq_)
|
||||
{
|
||||
assert(seq == latestSeq_ + 1 || latestSeq_ == 0);
|
||||
@@ -80,9 +96,9 @@ SimpleCache::getPredecessor(ripple::uint256 const& key, uint32_t seq) const
|
||||
std::optional<Blob>
|
||||
SimpleCache::get(ripple::uint256 const& key, uint32_t seq) const
|
||||
{
|
||||
std::shared_lock lck{mtx_};
|
||||
if (seq > latestSeq_)
|
||||
return {};
|
||||
std::shared_lock lck{mtx_};
|
||||
objectReqCounter_++;
|
||||
auto e = map_.find(key);
|
||||
if (e == map_.end())
|
||||
@@ -106,7 +122,7 @@ SimpleCache::setFull()
|
||||
return;
|
||||
|
||||
full_ = true;
|
||||
std::unique_lock lck{mtx_};
|
||||
std::scoped_lock lck{mtx_};
|
||||
deletes_.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef CLIO_SIMPLECACHE_H_INCLUDED
|
||||
#define CLIO_SIMPLECACHE_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/basics/hardened_hash.h>
|
||||
@@ -19,11 +37,11 @@ class SimpleCache
|
||||
};
|
||||
|
||||
// counters for fetchLedgerObject(s) hit rate
|
||||
mutable std::atomic_uint32_t objectReqCounter_;
|
||||
mutable std::atomic_uint32_t objectHitCounter_;
|
||||
mutable std::atomic_uint32_t objectReqCounter_ = 0;
|
||||
mutable std::atomic_uint32_t objectHitCounter_ = 0;
|
||||
// counters for fetchSuccessorKey hit rate
|
||||
mutable std::atomic_uint32_t successorReqCounter_;
|
||||
mutable std::atomic_uint32_t successorHitCounter_;
|
||||
mutable std::atomic_uint32_t successorReqCounter_ = 0;
|
||||
mutable std::atomic_uint32_t successorHitCounter_ = 0;
|
||||
|
||||
std::map<ripple::uint256, CacheEntry> map_;
|
||||
mutable std::shared_mutex mtx_;
|
||||
@@ -38,10 +56,7 @@ public:
|
||||
// Update the cache with new ledger objects
|
||||
// set isBackground to true when writing old data from a background thread
|
||||
void
|
||||
update(
|
||||
std::vector<LedgerObject> const& blobs,
|
||||
uint32_t seq,
|
||||
bool isBackground = false);
|
||||
update(std::vector<LedgerObject> const& blobs, uint32_t seq, bool isBackground = false);
|
||||
|
||||
std::optional<Blob>
|
||||
get(ripple::uint256 const& key, uint32_t seq) const;
|
||||
@@ -78,4 +93,3 @@ public:
|
||||
};
|
||||
|
||||
} // namespace Backend
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
#ifndef CLIO_TYPES_H_INCLUDED
|
||||
#define CLIO_TYPES_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <optional>
|
||||
@@ -37,8 +56,27 @@ struct TransactionAndMetadata
|
||||
{
|
||||
Blob transaction;
|
||||
Blob metadata;
|
||||
std::uint32_t ledgerSequence;
|
||||
std::uint32_t date;
|
||||
std::uint32_t ledgerSequence = 0;
|
||||
std::uint32_t date = 0;
|
||||
|
||||
TransactionAndMetadata() = default;
|
||||
TransactionAndMetadata(
|
||||
Blob const& transaction,
|
||||
Blob const& metadata,
|
||||
std::uint32_t ledgerSequence,
|
||||
std::uint32_t date)
|
||||
: transaction{transaction}, metadata{metadata}, ledgerSequence{ledgerSequence}, date{date}
|
||||
{
|
||||
}
|
||||
|
||||
TransactionAndMetadata(std::tuple<Blob, Blob, std::uint32_t, std::uint32_t> data)
|
||||
: transaction{std::get<0>(data)}
|
||||
, metadata{std::get<1>(data)}
|
||||
, ledgerSequence{std::get<2>(data)}
|
||||
, date{std::get<3>(data)}
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(const TransactionAndMetadata& other) const
|
||||
{
|
||||
@@ -51,6 +89,29 @@ struct TransactionsCursor
|
||||
{
|
||||
std::uint32_t ledgerSequence;
|
||||
std::uint32_t transactionIndex;
|
||||
|
||||
TransactionsCursor() = default;
|
||||
TransactionsCursor(std::uint32_t ledgerSequence, std::uint32_t transactionIndex)
|
||||
: ledgerSequence{ledgerSequence}, transactionIndex{transactionIndex}
|
||||
{
|
||||
}
|
||||
|
||||
TransactionsCursor(std::tuple<std::uint32_t, std::uint32_t> data)
|
||||
: ledgerSequence{std::get<0>(data)}, transactionIndex{std::get<1>(data)}
|
||||
{
|
||||
}
|
||||
|
||||
TransactionsCursor&
|
||||
operator=(TransactionsCursor const&) = default;
|
||||
|
||||
bool
|
||||
operator==(TransactionsCursor const& other) const = default;
|
||||
|
||||
[[nodiscard]] std::tuple<std::uint32_t, std::uint32_t>
|
||||
asTuple() const
|
||||
{
|
||||
return std::make_tuple(ledgerSequence, transactionIndex);
|
||||
}
|
||||
};
|
||||
|
||||
struct TransactionsAndCursor
|
||||
@@ -64,16 +125,31 @@ struct NFT
|
||||
ripple::uint256 tokenID;
|
||||
std::uint32_t ledgerSequence;
|
||||
ripple::AccountID owner;
|
||||
Blob uri;
|
||||
bool isBurned;
|
||||
|
||||
NFT() = default;
|
||||
NFT(ripple::uint256 const& tokenID,
|
||||
std::uint32_t ledgerSequence,
|
||||
ripple::AccountID const& owner,
|
||||
Blob const& uri,
|
||||
bool isBurned)
|
||||
: tokenID{tokenID}, ledgerSequence{ledgerSequence}, owner{owner}, uri{uri}, isBurned{isBurned}
|
||||
{
|
||||
}
|
||||
|
||||
NFT(ripple::uint256 const& tokenID, std::uint32_t ledgerSequence, ripple::AccountID const& owner, bool isBurned)
|
||||
: NFT(tokenID, ledgerSequence, owner, {}, isBurned)
|
||||
{
|
||||
}
|
||||
|
||||
// clearly two tokens are the same if they have the same ID, but this
|
||||
// struct stores the state of a given token at a given ledger sequence, so
|
||||
// we also need to compare with ledgerSequence
|
||||
bool
|
||||
operator==(NFT const& other) const
|
||||
{
|
||||
return tokenID == other.tokenID &&
|
||||
ledgerSequence == other.ledgerSequence;
|
||||
return tokenID == other.tokenID && ledgerSequence == other.ledgerSequence;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,11 +158,7 @@ struct LedgerRange
|
||||
std::uint32_t minSequence;
|
||||
std::uint32_t maxSequence;
|
||||
};
|
||||
constexpr ripple::uint256 firstKey{
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"};
|
||||
constexpr ripple::uint256 lastKey{
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"};
|
||||
constexpr ripple::uint256 hi192{
|
||||
"0000000000000000000000000000000000000000000000001111111111111111"};
|
||||
constexpr ripple::uint256 firstKey{"0000000000000000000000000000000000000000000000000000000000000000"};
|
||||
constexpr ripple::uint256 lastKey{"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"};
|
||||
constexpr ripple::uint256 hi192{"0000000000000000000000000000000000000000000000001111111111111111"};
|
||||
} // namespace Backend
|
||||
#endif
|
||||
|
||||
79
src/backend/cassandra/Concepts.h
Normal file
79
src/backend/cassandra/Concepts.h
Normal file
@@ -0,0 +1,79 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Types.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <concepts>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
// clang-format off
|
||||
template <typename T>
|
||||
concept SomeSettingsProvider = requires(T a) {
|
||||
{ a.getSettings() } -> std::same_as<Settings>;
|
||||
{ a.getKeyspace() } -> std::same_as<std::string>;
|
||||
{ a.getTablePrefix() } -> std::same_as<std::optional<std::string>>;
|
||||
{ a.getReplicationFactor() } -> std::same_as<uint16_t>;
|
||||
{ a.getTtl() } -> std::same_as<uint16_t>;
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// clang-format off
|
||||
template <typename T>
|
||||
concept SomeExecutionStrategy = requires(
|
||||
T a,
|
||||
Settings settings,
|
||||
Handle handle,
|
||||
Statement statement,
|
||||
std::vector<Statement> statements,
|
||||
PreparedStatement prepared,
|
||||
boost::asio::yield_context token
|
||||
) {
|
||||
{ T(settings, handle) };
|
||||
{ a.sync() } -> std::same_as<void>;
|
||||
{ a.isTooBusy() } -> std::same_as<bool>;
|
||||
{ a.writeSync(statement) } -> std::same_as<ResultOrError>;
|
||||
{ a.writeSync(prepared) } -> std::same_as<ResultOrError>;
|
||||
{ a.write(prepared) } -> std::same_as<void>;
|
||||
{ a.write(std::move(statements)) } -> std::same_as<void>;
|
||||
{ a.read(token, prepared) } -> std::same_as<ResultOrError>;
|
||||
{ a.read(token, statement) } -> std::same_as<ResultOrError>;
|
||||
{ a.read(token, statements) } -> std::same_as<ResultOrError>;
|
||||
{ a.readEach(token, statements) } -> std::same_as<std::vector<Result>>;
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// clang-format off
|
||||
template <typename T>
|
||||
concept SomeRetryPolicy = requires(T a, boost::asio::io_context ioc, CassandraError err, uint32_t attempt) {
|
||||
{ T(ioc) };
|
||||
{ a.shouldRetry(err) } -> std::same_as<bool>;
|
||||
{ a.retry([](){}) } -> std::same_as<void>;
|
||||
{ a.calculateDelay(attempt) } -> std::same_as<std::chrono::milliseconds>;
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
99
src/backend/cassandra/Error.h
Normal file
99
src/backend/cassandra/Error.h
Normal file
@@ -0,0 +1,99 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
/**
|
||||
* @brief A simple container for both error message and error code
|
||||
*/
|
||||
class CassandraError
|
||||
{
|
||||
std::string message_;
|
||||
uint32_t code_;
|
||||
|
||||
public:
|
||||
CassandraError() = default; // default constructible required by Expected
|
||||
CassandraError(std::string message, uint32_t code) : message_{message}, code_{code}
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend std::string
|
||||
operator+(T const& lhs, CassandraError const& rhs) requires std::is_convertible_v<T, std::string>
|
||||
{
|
||||
return lhs + rhs.message();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend bool
|
||||
operator==(T const& lhs, CassandraError const& rhs) requires std::is_convertible_v<T, std::string>
|
||||
{
|
||||
return lhs == rhs.message();
|
||||
}
|
||||
|
||||
template <std::integral T>
|
||||
friend bool
|
||||
operator==(T const& lhs, CassandraError const& rhs)
|
||||
{
|
||||
return lhs == rhs.code();
|
||||
}
|
||||
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& os, CassandraError const& err)
|
||||
{
|
||||
os << err.message();
|
||||
return os;
|
||||
}
|
||||
|
||||
std::string
|
||||
message() const
|
||||
{
|
||||
return message_;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
code() const
|
||||
{
|
||||
return code_;
|
||||
}
|
||||
|
||||
bool
|
||||
isTimeout() const
|
||||
{
|
||||
if (code_ == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or code_ == CASS_ERROR_LIB_REQUEST_TIMED_OUT or
|
||||
code_ == CASS_ERROR_SERVER_UNAVAILABLE or code_ == CASS_ERROR_SERVER_OVERLOADED or
|
||||
code_ == CASS_ERROR_SERVER_READ_TIMEOUT)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
isInvalidQuery() const
|
||||
{
|
||||
return code_ == CASS_ERROR_SERVER_INVALID_QUERY;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
155
src/backend/cassandra/Handle.cpp
Normal file
155
src/backend/cassandra/Handle.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/Handle.h>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
Handle::Handle(Settings clusterSettings) : cluster_{clusterSettings}
|
||||
{
|
||||
}
|
||||
|
||||
Handle::Handle(std::string_view contactPoints) : Handle{Settings::defaultSettings().withContactPoints(contactPoints)}
|
||||
{
|
||||
}
|
||||
|
||||
Handle::~Handle()
|
||||
{
|
||||
[[maybe_unused]] auto _ = disconnect(); // attempt to disconnect
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncConnect() const
|
||||
{
|
||||
return cass_session_connect(session_, cluster_);
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::connect() const
|
||||
{
|
||||
return asyncConnect().await();
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncConnect(std::string_view keyspace) const
|
||||
{
|
||||
return cass_session_connect_keyspace(session_, cluster_, keyspace.data());
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::connect(std::string_view keyspace) const
|
||||
{
|
||||
return asyncConnect(keyspace).await();
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncDisconnect() const
|
||||
{
|
||||
return cass_session_close(session_);
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::disconnect() const
|
||||
{
|
||||
return asyncDisconnect().await();
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncReconnect(std::string_view keyspace) const
|
||||
{
|
||||
if (auto rc = asyncDisconnect().await(); not rc) // sync
|
||||
throw std::logic_error("Reconnect to keyspace '" + std::string{keyspace} + "' failed: " + rc.error());
|
||||
return asyncConnect(keyspace);
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::reconnect(std::string_view keyspace) const
|
||||
{
|
||||
return asyncReconnect(keyspace).await();
|
||||
}
|
||||
|
||||
std::vector<Handle::FutureType>
|
||||
Handle::asyncExecuteEach(std::vector<Statement> const& statements) const
|
||||
{
|
||||
std::vector<Handle::FutureType> futures;
|
||||
for (auto const& statement : statements)
|
||||
futures.push_back(cass_session_execute(session_, statement));
|
||||
return futures;
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::executeEach(std::vector<Statement> const& statements) const
|
||||
{
|
||||
for (auto futures = asyncExecuteEach(statements); auto const& future : futures)
|
||||
{
|
||||
if (auto const rc = future.await(); not rc)
|
||||
return rc;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncExecute(Statement const& statement) const
|
||||
{
|
||||
return cass_session_execute(session_, statement);
|
||||
}
|
||||
|
||||
Handle::FutureWithCallbackType
|
||||
Handle::asyncExecute(Statement const& statement, std::function<void(Handle::ResultOrErrorType)>&& cb) const
|
||||
{
|
||||
return Handle::FutureWithCallbackType{cass_session_execute(session_, statement), std::move(cb)};
|
||||
}
|
||||
|
||||
Handle::ResultOrErrorType
|
||||
Handle::execute(Statement const& statement) const
|
||||
{
|
||||
return asyncExecute(statement).get();
|
||||
}
|
||||
|
||||
Handle::FutureType
|
||||
Handle::asyncExecute(std::vector<Statement> const& statements) const
|
||||
{
|
||||
return cass_session_execute_batch(session_, Batch{statements});
|
||||
}
|
||||
|
||||
Handle::MaybeErrorType
|
||||
Handle::execute(std::vector<Statement> const& statements) const
|
||||
{
|
||||
return asyncExecute(statements).await();
|
||||
}
|
||||
|
||||
Handle::FutureWithCallbackType
|
||||
Handle::asyncExecute(std::vector<Statement> const& statements, std::function<void(Handle::ResultOrErrorType)>&& cb)
|
||||
const
|
||||
{
|
||||
return Handle::FutureWithCallbackType{cass_session_execute_batch(session_, Batch{statements}), std::move(cb)};
|
||||
}
|
||||
|
||||
Handle::PreparedStatementType
|
||||
Handle::prepare(std::string_view query) const
|
||||
{
|
||||
Handle::FutureType future = cass_session_prepare(session_, query.data());
|
||||
if (auto const rc = future.await(); rc)
|
||||
return cass_future_get_prepared(future);
|
||||
else
|
||||
throw std::runtime_error(rc.error().message());
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
295
src/backend/cassandra/Handle.h
Normal file
295
src/backend/cassandra/Handle.h
Normal file
@@ -0,0 +1,295 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Error.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/Batch.h>
|
||||
#include <backend/cassandra/impl/Cluster.h>
|
||||
#include <backend/cassandra/impl/Future.h>
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
#include <backend/cassandra/impl/Result.h>
|
||||
#include <backend/cassandra/impl/Session.h>
|
||||
#include <backend/cassandra/impl/Statement.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <compare>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
/**
|
||||
* @brief Represents a handle to the cassandra database cluster
|
||||
*/
|
||||
class Handle
|
||||
{
|
||||
detail::Cluster cluster_;
|
||||
detail::Session session_;
|
||||
|
||||
public:
|
||||
using ResultOrErrorType = ResultOrError;
|
||||
using MaybeErrorType = MaybeError;
|
||||
using FutureWithCallbackType = FutureWithCallback;
|
||||
using FutureType = Future;
|
||||
using StatementType = Statement;
|
||||
using PreparedStatementType = PreparedStatement;
|
||||
using ResultType = Result;
|
||||
|
||||
/**
|
||||
* @brief Construct a new handle from a @ref Settings object
|
||||
*/
|
||||
explicit Handle(Settings clusterSettings = Settings::defaultSettings());
|
||||
|
||||
/**
|
||||
* @brief Construct a new handle with default settings and only by setting
|
||||
* the contact points
|
||||
*/
|
||||
explicit Handle(std::string_view contactPoints);
|
||||
|
||||
/**
|
||||
* @brief Disconnects gracefully if possible
|
||||
*/
|
||||
~Handle();
|
||||
|
||||
/**
|
||||
* @brief Move is supported
|
||||
*/
|
||||
Handle(Handle&&) = default;
|
||||
|
||||
/**
|
||||
* @brief Connect to the cluster asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncConnect() const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncConnect() const for how this works.
|
||||
*/
|
||||
[[nodiscard]] MaybeErrorType
|
||||
connect() const;
|
||||
|
||||
/**
|
||||
* @brief Connect to the the specified keyspace asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncConnect(std::string_view keyspace) const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncConnect(std::string_view) const for how this works.
|
||||
*/
|
||||
[[nodiscard]] MaybeErrorType
|
||||
connect(std::string_view keyspace) const;
|
||||
|
||||
/**
|
||||
* @brief Disconnect from the cluster asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncDisconnect() const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncDisconnect() const for how this works.
|
||||
*/
|
||||
[[maybe_unused]] MaybeErrorType
|
||||
disconnect() const;
|
||||
|
||||
/**
|
||||
* @brief Reconnect to the the specified keyspace asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncReconnect(std::string_view keyspace) const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncReconnect(std::string_view) const for how this works.
|
||||
*/
|
||||
[[nodiscard]] MaybeErrorType
|
||||
reconnect(std::string_view keyspace) const;
|
||||
|
||||
/**
|
||||
* @brief Execute a simple query with optional args asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[nodiscard]] FutureType
|
||||
asyncExecute(std::string_view query, Args&&... args) const
|
||||
{
|
||||
auto statement = StatementType{query, std::forward<Args>(args)...};
|
||||
return cass_session_execute(session_, statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncExecute(std::string_view, Args&&...) const for how this
|
||||
* works.
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
execute(std::string_view query, Args&&... args) const
|
||||
{
|
||||
return asyncExecute<Args...>(query, std::forward<Args>(args)...).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Execute each of the statements asynchronously
|
||||
*
|
||||
* Batched version is not always the right option. Especially since it only
|
||||
* supports INSERT, UPDATE and DELETE statements.
|
||||
* This can be used as an alternative when statements need to execute in
|
||||
* bulk.
|
||||
*
|
||||
* @return A vector of future objects
|
||||
*/
|
||||
[[nodiscard]] std::vector<FutureType>
|
||||
asyncExecuteEach(std::vector<StatementType> const& statements) const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncExecuteEach(std::vector<StatementType> const&) const for
|
||||
* how this works.
|
||||
*/
|
||||
[[maybe_unused]] MaybeErrorType
|
||||
executeEach(std::vector<StatementType> const& statements) const;
|
||||
|
||||
/**
|
||||
* @brief Execute a prepared statement with optional args asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[nodiscard]] FutureType
|
||||
asyncExecute(PreparedStatementType const& statement, Args&&... args) const
|
||||
{
|
||||
auto bound = statement.bind<Args...>(std::forward<Args>(args)...);
|
||||
return cass_session_execute(session_, bound);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncExecute(std::vector<StatementType> const&, Args&&...) const
|
||||
* for how this works.
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
execute(PreparedStatementType const& statement, Args&&... args) const
|
||||
{
|
||||
return asyncExecute<Args...>(statement, std::forward<Args>(args)...).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Execute one (bound or simple) statements asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncExecute(StatementType const& statement) const;
|
||||
|
||||
/**
|
||||
* @brief Execute one (bound or simple) statements asynchronously with a
|
||||
* callback
|
||||
*
|
||||
* @return A future that holds onto the callback provided
|
||||
*/
|
||||
[[nodiscard]] FutureWithCallbackType
|
||||
asyncExecute(StatementType const& statement, std::function<void(ResultOrErrorType)>&& cb) const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncExecute(StatementType const&) const for how this
|
||||
* works.
|
||||
*/
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
execute(StatementType const& statement) const;
|
||||
|
||||
/**
|
||||
* @brief Execute a batch of (bound or simple) statements asynchronously
|
||||
*
|
||||
* @return A future
|
||||
*/
|
||||
[[nodiscard]] FutureType
|
||||
asyncExecute(std::vector<StatementType> const& statements) const;
|
||||
|
||||
/**
|
||||
* @brief Synchonous version of the above
|
||||
*
|
||||
* See @ref asyncExecute(std::vector<StatementType> const&) const for how
|
||||
* this works.
|
||||
*/
|
||||
[[maybe_unused]] MaybeErrorType
|
||||
execute(std::vector<StatementType> const& statements) const;
|
||||
|
||||
/**
|
||||
* @brief Execute a batch of (bound or simple) statements asynchronously
|
||||
* with a completion callback
|
||||
*
|
||||
* @return A future that holds onto the callback provided
|
||||
*/
|
||||
[[nodiscard]] FutureWithCallbackType
|
||||
asyncExecute(std::vector<StatementType> const& statements, std::function<void(ResultOrErrorType)>&& cb) const;
|
||||
|
||||
/**
|
||||
* @brief Prepare a statement
|
||||
*
|
||||
* @return A @ref PreparedStatementType
|
||||
* @throws std::runtime_error with underlying error description on failure
|
||||
*/
|
||||
[[nodiscard]] PreparedStatementType
|
||||
prepare(std::string_view query) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Extracts the results into series of std::tuple<Types...> by creating a
|
||||
* simple wrapper with an STL input iterator inside.
|
||||
*
|
||||
* You can call .begin() and .end() in order to iterate as usual.
|
||||
* This also means that you can use it in a range-based for or with some
|
||||
* algorithms.
|
||||
*/
|
||||
template <typename... Types>
|
||||
[[nodiscard]] detail::ResultExtractor<Types...>
|
||||
extract(Handle::ResultType const& result)
|
||||
{
|
||||
return {result};
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
667
src/backend/cassandra/Schema.h
Normal file
667
src/backend/cassandra/Schema.h
Normal file
@@ -0,0 +1,667 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Concepts.h>
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/SettingsProvider.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <fmt/compile.h>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
template <SomeSettingsProvider SettingsProviderType>
|
||||
[[nodiscard]] std::string inline qualifiedTableName(SettingsProviderType const& provider, std::string_view name)
|
||||
{
|
||||
return fmt::format("{}.{}{}", provider.getKeyspace(), provider.getTablePrefix().value_or(""), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Manages the DB schema and provides access to prepared statements
|
||||
*/
|
||||
template <SomeSettingsProvider SettingsProviderType>
|
||||
class Schema
|
||||
{
|
||||
// Current schema version.
|
||||
// Update this everytime you update the schema.
|
||||
// Migrations will be ran automatically based on this value.
|
||||
static constexpr uint16_t version = 1u;
|
||||
|
||||
clio::Logger log_{"Backend"};
|
||||
std::reference_wrapper<SettingsProviderType const> settingsProvider_;
|
||||
|
||||
public:
|
||||
explicit Schema(SettingsProviderType const& settingsProvider) : settingsProvider_{std::cref(settingsProvider)}
|
||||
{
|
||||
}
|
||||
|
||||
std::string createKeyspace = [this]() {
|
||||
return fmt::format(
|
||||
R"(
|
||||
CREATE KEYSPACE IF NOT EXISTS {}
|
||||
WITH replication = {{
|
||||
'class': 'SimpleStrategy',
|
||||
'replication_factor': '{}'
|
||||
}}
|
||||
AND durable_writes = true
|
||||
)",
|
||||
settingsProvider_.get().getKeyspace(),
|
||||
settingsProvider_.get().getReplicationFactor());
|
||||
}();
|
||||
|
||||
// =======================
|
||||
// Schema creation queries
|
||||
// =======================
|
||||
|
||||
std::vector<Statement> createSchema = [this]() {
|
||||
std::vector<Statement> statements;
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
key blob,
|
||||
sequence bigint,
|
||||
object blob,
|
||||
PRIMARY KEY (key, sequence)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (sequence DESC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
hash blob PRIMARY KEY,
|
||||
ledger_sequence bigint,
|
||||
date bigint,
|
||||
transaction blob,
|
||||
metadata blob
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "transactions"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
ledger_sequence bigint,
|
||||
hash blob,
|
||||
PRIMARY KEY (ledger_sequence, hash)
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_transactions"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
key blob,
|
||||
seq bigint,
|
||||
next blob,
|
||||
PRIMARY KEY (key, seq)
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "successor"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
seq bigint,
|
||||
key blob,
|
||||
PRIMARY KEY (seq, key)
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "diff"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
account blob,
|
||||
seq_idx tuple<bigint, bigint>,
|
||||
hash blob,
|
||||
PRIMARY KEY (account, seq_idx)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (seq_idx DESC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "account_tx"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
sequence bigint PRIMARY KEY,
|
||||
header blob
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledgers"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
hash blob PRIMARY KEY,
|
||||
sequence bigint
|
||||
)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_hashes"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
is_latest boolean PRIMARY KEY,
|
||||
sequence bigint
|
||||
)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
token_id blob,
|
||||
sequence bigint,
|
||||
owner blob,
|
||||
is_burned boolean,
|
||||
PRIMARY KEY (token_id, sequence)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (sequence DESC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_tokens"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
issuer blob,
|
||||
taxon bigint,
|
||||
token_id blob,
|
||||
PRIMARY KEY (issuer, taxon, token_id)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (taxon ASC, token_id ASC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
token_id blob,
|
||||
sequence bigint,
|
||||
uri blob,
|
||||
PRIMARY KEY (token_id, sequence)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (sequence DESC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_uris"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
token_id blob,
|
||||
seq_idx tuple<bigint, bigint>,
|
||||
hash blob,
|
||||
PRIMARY KEY (token_id, seq_idx)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (seq_idx DESC)
|
||||
AND default_time_to_live = {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions"),
|
||||
settingsProvider_.get().getTtl()));
|
||||
|
||||
return statements;
|
||||
}();
|
||||
|
||||
/**
|
||||
* @brief Prepared statements holder
|
||||
*/
|
||||
class Statements
|
||||
{
|
||||
std::reference_wrapper<SettingsProviderType const> settingsProvider_;
|
||||
std::reference_wrapper<Handle const> handle_;
|
||||
|
||||
public:
|
||||
Statements(SettingsProviderType const& settingsProvider, Handle const& handle)
|
||||
: settingsProvider_{settingsProvider}, handle_{std::cref(handle)}
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
// Insert queries
|
||||
//
|
||||
|
||||
PreparedStatement insertObject = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(key, sequence, object)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertTransaction = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(hash, ledger_sequence, date, transaction, metadata)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertLedgerTransaction = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(ledger_sequence, hash)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertSuccessor = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(key, seq, next)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "successor")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertDiff = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(seq, key)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "diff")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertAccountTx = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(account, seq_idx, hash)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "account_tx")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertNFT = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(token_id, sequence, owner, is_burned)
|
||||
VALUES (?, ?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_tokens")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertIssuerNFT = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(issuer, taxon, token_id)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertNFTURI = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(token_id, sequence, uri)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_uris")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertNFTTx = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(token_id, seq_idx, hash)
|
||||
VALUES (?, ?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertLedgerHeader = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(sequence, header)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledgers")));
|
||||
}();
|
||||
|
||||
PreparedStatement insertLedgerHash = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(hash, sequence)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_hashes")));
|
||||
}();
|
||||
|
||||
//
|
||||
// Update (and "delete") queries
|
||||
//
|
||||
|
||||
PreparedStatement updateLedgerRange = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
UPDATE {}
|
||||
SET sequence = ?
|
||||
WHERE is_latest = ?
|
||||
IF sequence IN (?, null)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
|
||||
}();
|
||||
|
||||
PreparedStatement deleteLedgerRange = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
UPDATE {}
|
||||
SET sequence = ?
|
||||
WHERE is_latest = false
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
|
||||
}();
|
||||
|
||||
//
|
||||
// Select queries
|
||||
//
|
||||
|
||||
PreparedStatement selectSuccessor = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT next
|
||||
FROM {}
|
||||
WHERE key = ?
|
||||
AND seq <= ?
|
||||
ORDER BY seq DESC
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "successor")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectDiff = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT key
|
||||
FROM {}
|
||||
WHERE seq = ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "diff")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectObject = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT object, sequence
|
||||
FROM {}
|
||||
WHERE key = ?
|
||||
AND sequence <= ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectTransaction = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT transaction, metadata, ledger_sequence, date
|
||||
FROM {}
|
||||
WHERE hash = ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectAllTransactionHashesInLedger = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT hash
|
||||
FROM {}
|
||||
WHERE ledger_sequence = ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerPageKeys = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT key
|
||||
FROM {}
|
||||
WHERE TOKEN(key) >= ?
|
||||
AND sequence <= ?
|
||||
PER PARTITION LIMIT 1
|
||||
LIMIT ?
|
||||
ALLOW FILTERING
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerPage = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT object, key
|
||||
FROM {}
|
||||
WHERE TOKEN(key) >= ?
|
||||
AND sequence <= ?
|
||||
PER PARTITION LIMIT 1
|
||||
LIMIT ?
|
||||
ALLOW FILTERING
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects")));
|
||||
}();
|
||||
|
||||
PreparedStatement getToken = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT TOKEN(key)
|
||||
FROM {}
|
||||
WHERE key = ?
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "objects")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectAccountTx = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT hash, seq_idx
|
||||
FROM {}
|
||||
WHERE account = ?
|
||||
AND seq_idx <= ?
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "account_tx")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectAccountTxForward = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT hash, seq_idx
|
||||
FROM {}
|
||||
WHERE account = ?
|
||||
AND seq_idx >= ?
|
||||
ORDER BY seq_idx ASC
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "account_tx")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectNFT = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT sequence, owner, is_burned
|
||||
FROM {}
|
||||
WHERE token_id = ?
|
||||
AND sequence <= ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_tokens")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectNFTURI = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT uri
|
||||
FROM {}
|
||||
WHERE token_id = ?
|
||||
AND sequence <= ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_uris")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectNFTTx = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT hash, seq_idx
|
||||
FROM {}
|
||||
WHERE token_id = ?
|
||||
AND seq_idx < ?
|
||||
ORDER BY seq_idx DESC
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectNFTTxForward = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT hash, seq_idx
|
||||
FROM {}
|
||||
WHERE token_id = ?
|
||||
AND seq_idx >= ?
|
||||
ORDER BY seq_idx ASC
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerByHash = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT sequence
|
||||
FROM {}
|
||||
WHERE hash = ?
|
||||
LIMIT 1
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_hashes")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerBySeq = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT header
|
||||
FROM {}
|
||||
WHERE sequence = ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledgers")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLatestLedger = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT sequence
|
||||
FROM {}
|
||||
WHERE is_latest = true
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerRange = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT sequence
|
||||
FROM {}
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
|
||||
}();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Recreates the prepared statements
|
||||
*/
|
||||
void
|
||||
prepareStatements(Handle const& handle)
|
||||
{
|
||||
log_.info() << "Preparing cassandra statements";
|
||||
statements_ = std::make_unique<Statements>(settingsProvider_, handle);
|
||||
log_.info() << "Finished preparing statements";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Provides access to statements
|
||||
*/
|
||||
std::unique_ptr<Statements> const&
|
||||
operator->() const
|
||||
{
|
||||
return statements_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Statements> statements_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
125
src/backend/cassandra/SettingsProvider.cpp
Normal file
125
src/backend/cassandra/SettingsProvider.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/SettingsProvider.h>
|
||||
#include <backend/cassandra/impl/Cluster.h>
|
||||
#include <backend/cassandra/impl/Statement.h>
|
||||
#include <config/Config.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
namespace detail {
|
||||
inline Settings::ContactPoints
|
||||
tag_invoke(boost::json::value_to_tag<Settings::ContactPoints>, boost::json::value const& value)
|
||||
{
|
||||
if (not value.is_object())
|
||||
throw std::runtime_error(
|
||||
"Feed entire Cassandra section to parse "
|
||||
"Settings::ContactPoints instead");
|
||||
|
||||
clio::Config obj{value};
|
||||
Settings::ContactPoints out;
|
||||
|
||||
out.contactPoints = obj.valueOrThrow<std::string>("contact_points", "`contact_points` must be a string");
|
||||
out.port = obj.maybeValue<uint16_t>("port");
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
inline Settings::SecureConnectionBundle
|
||||
tag_invoke(boost::json::value_to_tag<Settings::SecureConnectionBundle>, boost::json::value const& value)
|
||||
{
|
||||
if (not value.is_string())
|
||||
throw std::runtime_error("`secure_connect_bundle` must be a string");
|
||||
return Settings::SecureConnectionBundle{value.as_string().data()};
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
SettingsProvider::SettingsProvider(clio::Config const& cfg, uint16_t ttl)
|
||||
: config_{cfg}
|
||||
, keyspace_{cfg.valueOr<std::string>("keyspace", "clio")}
|
||||
, tablePrefix_{cfg.maybeValue<std::string>("table_prefix")}
|
||||
, replicationFactor_{cfg.valueOr<uint16_t>("replication_factor", 3)}
|
||||
, ttl_{ttl}
|
||||
, settings_{parseSettings()}
|
||||
{
|
||||
}
|
||||
|
||||
Settings
|
||||
SettingsProvider::getSettings() const
|
||||
{
|
||||
return settings_;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
SettingsProvider::parseOptionalCertificate() const
|
||||
{
|
||||
if (auto const certPath = config_.maybeValue<std::string>("certfile"); certPath)
|
||||
{
|
||||
auto const path = std::filesystem::path(*certPath);
|
||||
std::ifstream fileStream(path.string(), std::ios::in);
|
||||
if (!fileStream)
|
||||
{
|
||||
throw std::system_error(errno, std::generic_category(), "Opening certificate " + path.string());
|
||||
}
|
||||
|
||||
std::string contents(std::istreambuf_iterator<char>{fileStream}, std::istreambuf_iterator<char>{});
|
||||
if (fileStream.bad())
|
||||
{
|
||||
throw std::system_error(errno, std::generic_category(), "Reading certificate " + path.string());
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Settings
|
||||
SettingsProvider::parseSettings() const
|
||||
{
|
||||
auto settings = Settings::defaultSettings();
|
||||
if (auto const bundle = config_.maybeValue<Settings::SecureConnectionBundle>("secure_connect_bundle"); bundle)
|
||||
{
|
||||
settings.connectionInfo = *bundle;
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.connectionInfo =
|
||||
config_.valueOrThrow<Settings::ContactPoints>("Missing contact_points in Cassandra config");
|
||||
}
|
||||
|
||||
settings.threads = config_.valueOr<uint32_t>("threads", settings.threads);
|
||||
settings.maxWriteRequestsOutstanding =
|
||||
config_.valueOr<uint32_t>("max_write_requests_outstanding", settings.maxWriteRequestsOutstanding);
|
||||
settings.maxReadRequestsOutstanding =
|
||||
config_.valueOr<uint32_t>("max_read_requests_outstanding", settings.maxReadRequestsOutstanding);
|
||||
settings.certificate = parseOptionalCertificate();
|
||||
settings.username = config_.maybeValue<std::string>("username");
|
||||
settings.password = config_.maybeValue<std::string>("password");
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
86
src/backend/cassandra/SettingsProvider.h
Normal file
86
src/backend/cassandra/SettingsProvider.h
Normal file
@@ -0,0 +1,86 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
/**
|
||||
* @brief Provides settings for @ref CassandraBackend
|
||||
*/
|
||||
class SettingsProvider
|
||||
{
|
||||
clio::Config config_;
|
||||
|
||||
std::string keyspace_;
|
||||
std::optional<std::string> tablePrefix_;
|
||||
uint16_t replicationFactor_;
|
||||
uint16_t ttl_;
|
||||
Settings settings_;
|
||||
|
||||
public:
|
||||
explicit SettingsProvider(clio::Config const& cfg, uint16_t ttl = 0);
|
||||
|
||||
/*! Get the cluster settings */
|
||||
[[nodiscard]] Settings
|
||||
getSettings() const;
|
||||
|
||||
/*! Get the specified keyspace */
|
||||
[[nodiscard]] inline std::string
|
||||
getKeyspace() const
|
||||
{
|
||||
return keyspace_;
|
||||
}
|
||||
|
||||
/*! Get an optional table prefix to use in all queries */
|
||||
[[nodiscard]] inline std::optional<std::string>
|
||||
getTablePrefix() const
|
||||
{
|
||||
return tablePrefix_;
|
||||
}
|
||||
|
||||
/*! Get the replication factor */
|
||||
[[nodiscard]] inline uint16_t
|
||||
getReplicationFactor() const
|
||||
{
|
||||
return replicationFactor_;
|
||||
}
|
||||
|
||||
/*! Get the default time to live to use in all `create` queries */
|
||||
[[nodiscard]] inline uint16_t
|
||||
getTtl() const
|
||||
{
|
||||
return ttl_;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
parseOptionalCertificate() const;
|
||||
|
||||
[[nodiscard]] Settings
|
||||
parseSettings() const;
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
67
src/backend/cassandra/Types.h
Normal file
67
src/backend/cassandra/Types.h
Normal file
@@ -0,0 +1,67 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Backend::Cassandra {
|
||||
|
||||
namespace detail {
|
||||
struct Settings;
|
||||
class Session;
|
||||
class Cluster;
|
||||
struct Future;
|
||||
class FutureWithCallback;
|
||||
struct Result;
|
||||
class Statement;
|
||||
class PreparedStatement;
|
||||
struct Batch;
|
||||
} // namespace detail
|
||||
|
||||
using Settings = detail::Settings;
|
||||
using Future = detail::Future;
|
||||
using FutureWithCallback = detail::FutureWithCallback;
|
||||
using Result = detail::Result;
|
||||
using Statement = detail::Statement;
|
||||
using PreparedStatement = detail::PreparedStatement;
|
||||
using Batch = detail::Batch;
|
||||
|
||||
/**
|
||||
* @brief A strong type wrapper for int32_t
|
||||
*
|
||||
* This is unfortunately needed right now to support uint32_t properly
|
||||
* because clio uses bigint (int64) everywhere except for when one need
|
||||
* to specify LIMIT, which needs an int32 :-/
|
||||
*/
|
||||
struct Limit
|
||||
{
|
||||
int32_t limit;
|
||||
};
|
||||
|
||||
class Handle;
|
||||
class CassandraError;
|
||||
|
||||
using MaybeError = util::Expected<void, CassandraError>;
|
||||
using ResultOrError = util::Expected<Result, CassandraError>;
|
||||
using Error = util::Unexpected<CassandraError>;
|
||||
|
||||
} // namespace Backend::Cassandra
|
||||
119
src/backend/cassandra/impl/AsyncExecutor.h
Normal file
119
src/backend/cassandra/impl/AsyncExecutor.h
Normal file
@@ -0,0 +1,119 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Concepts.h>
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/RetryPolicy.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/**
|
||||
* @brief A query executor with a changable retry policy
|
||||
*
|
||||
* Note: this is a bit of an anti-pattern and should be done differently
|
||||
* eventually.
|
||||
*
|
||||
* Currently it's basically a saner implementation of the previous design that
|
||||
* was used in production without much issue but was using raw new/delete and
|
||||
* could leak easily. This version is slightly better but the overall design is
|
||||
* flawed and should be reworked.
|
||||
*/
|
||||
template <
|
||||
typename StatementType,
|
||||
typename HandleType = Handle,
|
||||
SomeRetryPolicy RetryPolicyType = ExponentialBackoffRetryPolicy>
|
||||
class AsyncExecutor : public std::enable_shared_from_this<AsyncExecutor<StatementType, HandleType, RetryPolicyType>>
|
||||
{
|
||||
using FutureWithCallbackType = typename HandleType::FutureWithCallbackType;
|
||||
using CallbackType = std::function<void(typename HandleType::ResultOrErrorType)>;
|
||||
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
StatementType data_;
|
||||
RetryPolicyType retryPolicy_;
|
||||
CallbackType onComplete_;
|
||||
|
||||
// does not exist during initial construction, hence optional
|
||||
std::optional<FutureWithCallbackType> future_;
|
||||
std::mutex mtx_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new instance of the AsyncExecutor and execute it.
|
||||
*/
|
||||
static void
|
||||
run(boost::asio::io_context& ioc, HandleType const& handle, StatementType&& data, CallbackType&& onComplete)
|
||||
{
|
||||
// this is a helper that allows us to use std::make_shared below
|
||||
struct EnableMakeShared : public AsyncExecutor<StatementType, HandleType, RetryPolicyType>
|
||||
{
|
||||
EnableMakeShared(boost::asio::io_context& ioc, StatementType&& data, CallbackType&& onComplete)
|
||||
: AsyncExecutor(ioc, std::move(data), std::move(onComplete))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
auto ptr = std::make_shared<EnableMakeShared>(ioc, std::move(data), std::move(onComplete));
|
||||
ptr->execute(handle);
|
||||
}
|
||||
|
||||
private:
|
||||
AsyncExecutor(boost::asio::io_context& ioc, StatementType&& data, CallbackType&& onComplete)
|
||||
: data_{std::move(data)}, retryPolicy_{ioc}, onComplete_{std::move(onComplete)}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
execute(HandleType const& handle)
|
||||
{
|
||||
auto self = this->shared_from_this();
|
||||
|
||||
// lifetime is extended by capturing self ptr
|
||||
auto handler = [this, &handle, self](auto&& res) mutable {
|
||||
if (res)
|
||||
{
|
||||
onComplete_(std::move(res));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (retryPolicy_.shouldRetry(res.error()))
|
||||
retryPolicy_.retry([self, &handle]() { self->execute(handle); });
|
||||
else
|
||||
onComplete_(std::move(res)); // report error
|
||||
}
|
||||
|
||||
self = nullptr; // explicitly decrement refcount
|
||||
};
|
||||
|
||||
std::scoped_lock lck{mtx_};
|
||||
future_.emplace(handle.asyncExecute(data_, std::move(handler)));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
56
src/backend/cassandra/impl/Batch.cpp
Normal file
56
src/backend/cassandra/impl/Batch.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/Error.h>
|
||||
#include <backend/cassandra/impl/Batch.h>
|
||||
#include <backend/cassandra/impl/Statement.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <exception>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
static constexpr auto batchDeleter = [](CassBatch* ptr) { cass_batch_free(ptr); };
|
||||
};
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
// todo: use an appropritae value instead of CASS_BATCH_TYPE_LOGGED for
|
||||
// different use cases
|
||||
Batch::Batch(std::vector<Statement> const& statements)
|
||||
: ManagedObject{cass_batch_new(CASS_BATCH_TYPE_LOGGED), batchDeleter}
|
||||
{
|
||||
cass_batch_set_is_idempotent(*this, cass_true);
|
||||
|
||||
for (auto const& statement : statements)
|
||||
if (auto const res = add(statement); not res)
|
||||
throw std::runtime_error("Failed to add statement to batch: " + res.error());
|
||||
}
|
||||
|
||||
MaybeError
|
||||
Batch::add(Statement const& statement)
|
||||
{
|
||||
if (auto const rc = cass_batch_add_statement(*this, statement); rc != CASS_OK)
|
||||
{
|
||||
return Error{CassandraError{cass_error_desc(rc), rc}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
37
src/backend/cassandra/impl/Batch.h
Normal file
37
src/backend/cassandra/impl/Batch.h
Normal file
@@ -0,0 +1,37 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
struct Batch : public ManagedObject<CassBatch>
|
||||
{
|
||||
Batch(std::vector<Statement> const& statements);
|
||||
|
||||
MaybeError
|
||||
add(Statement const& statement);
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
154
src/backend/cassandra/impl/Cluster.cpp
Normal file
154
src/backend/cassandra/impl/Cluster.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/impl/Cluster.h>
|
||||
#include <backend/cassandra/impl/SslContext.h>
|
||||
#include <backend/cassandra/impl/Statement.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <exception>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
static constexpr auto clusterDeleter = [](CassCluster* ptr) { cass_cluster_free(ptr); };
|
||||
|
||||
template <class... Ts>
|
||||
struct overloadSet : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
// explicit deduction guide (not needed as of C++20, but clang be clang)
|
||||
template <class... Ts>
|
||||
overloadSet(Ts...) -> overloadSet<Ts...>;
|
||||
}; // namespace
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
Cluster::Cluster(Settings const& settings) : ManagedObject{cass_cluster_new(), clusterDeleter}
|
||||
{
|
||||
using std::to_string;
|
||||
|
||||
cass_cluster_set_token_aware_routing(*this, cass_true);
|
||||
if (auto const rc = cass_cluster_set_protocol_version(*this, CASS_PROTOCOL_VERSION_V4); rc != CASS_OK)
|
||||
{
|
||||
throw std::runtime_error(std::string{"Error setting cassandra protocol version to v4: "} + cass_error_desc(rc));
|
||||
}
|
||||
|
||||
if (auto const rc = cass_cluster_set_num_threads_io(*this, settings.threads); rc != CASS_OK)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string{"Error setting cassandra io threads to "} + to_string(settings.threads) + ": " +
|
||||
cass_error_desc(rc));
|
||||
}
|
||||
|
||||
cass_log_set_level(settings.enableLog ? CASS_LOG_TRACE : CASS_LOG_DISABLED);
|
||||
cass_cluster_set_connect_timeout(*this, settings.connectionTimeout.count());
|
||||
cass_cluster_set_request_timeout(*this, settings.requestTimeout.count());
|
||||
|
||||
// TODO: other options to experiment with and consider later:
|
||||
// cass_cluster_set_max_concurrent_requests_threshold(*this, 10000);
|
||||
// cass_cluster_set_queue_size_event(*this, 100000);
|
||||
// cass_cluster_set_queue_size_io(*this, 100000);
|
||||
// cass_cluster_set_write_bytes_high_water_mark(*this, 16 * 1024 * 1024); // 16mb
|
||||
// cass_cluster_set_write_bytes_low_water_mark(*this, 8 * 1024 * 1024); // half of allowance
|
||||
// cass_cluster_set_pending_requests_high_water_mark(*this, 5000);
|
||||
// cass_cluster_set_pending_requests_low_water_mark(*this, 2500); // half
|
||||
// cass_cluster_set_max_requests_per_flush(*this, 1000);
|
||||
// cass_cluster_set_max_concurrent_creation(*this, 8);
|
||||
// cass_cluster_set_max_connections_per_host(*this, 6);
|
||||
// cass_cluster_set_core_connections_per_host(*this, 4);
|
||||
// cass_cluster_set_constant_speculative_execution_policy(*this, 1000, 1024);
|
||||
|
||||
if (auto const rc = cass_cluster_set_queue_size_io(
|
||||
*this, settings.maxWriteRequestsOutstanding + settings.maxReadRequestsOutstanding);
|
||||
rc != CASS_OK)
|
||||
{
|
||||
throw std::runtime_error(std::string{"Could not set queue size for IO per host: "} + cass_error_desc(rc));
|
||||
}
|
||||
|
||||
setupConnection(settings);
|
||||
setupCertificate(settings);
|
||||
setupCredentials(settings);
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::setupConnection(Settings const& settings)
|
||||
{
|
||||
std::visit(
|
||||
overloadSet{
|
||||
[this](Settings::ContactPoints const& points) { setupContactPoints(points); },
|
||||
[this](Settings::SecureConnectionBundle const& bundle) { setupSecureBundle(bundle); }},
|
||||
settings.connectionInfo);
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::setupContactPoints(Settings::ContactPoints const& points)
|
||||
{
|
||||
using std::to_string;
|
||||
auto throwErrorIfNeeded = [](CassError rc, std::string const& label, std::string const& value) {
|
||||
if (rc != CASS_OK)
|
||||
throw std::runtime_error("Cassandra: Error setting " + label + " [" + value + "]: " + cass_error_desc(rc));
|
||||
};
|
||||
|
||||
{
|
||||
log_.debug() << "Attempt connection using contact points: " << points.contactPoints;
|
||||
auto const rc = cass_cluster_set_contact_points(*this, points.contactPoints.data());
|
||||
throwErrorIfNeeded(rc, "contact_points", points.contactPoints);
|
||||
}
|
||||
|
||||
if (points.port)
|
||||
{
|
||||
auto const rc = cass_cluster_set_port(*this, points.port.value());
|
||||
throwErrorIfNeeded(rc, "port", to_string(points.port.value()));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::setupSecureBundle(Settings::SecureConnectionBundle const& bundle)
|
||||
{
|
||||
log_.debug() << "Attempt connection using secure bundle";
|
||||
if (auto const rc = cass_cluster_set_cloud_secure_connection_bundle(*this, bundle.bundle.data()); rc != CASS_OK)
|
||||
{
|
||||
throw std::runtime_error("Failed to connect using secure connection bundle" + bundle.bundle);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::setupCertificate(Settings const& settings)
|
||||
{
|
||||
if (not settings.certificate)
|
||||
return;
|
||||
|
||||
log_.debug() << "Configure SSL context";
|
||||
SslContext context = SslContext(*settings.certificate);
|
||||
cass_cluster_set_ssl(*this, context);
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::setupCredentials(Settings const& settings)
|
||||
{
|
||||
if (not settings.username || not settings.password)
|
||||
return;
|
||||
|
||||
log_.debug() << "Set credentials; username: " << settings.username.value();
|
||||
cass_cluster_set_credentials(*this, settings.username.value().c_str(), settings.password.value().c_str());
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
99
src/backend/cassandra/impl/Cluster.h
Normal file
99
src/backend/cassandra/impl/Cluster.h
Normal file
@@ -0,0 +1,99 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <variant>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
struct Settings
|
||||
{
|
||||
struct ContactPoints
|
||||
{
|
||||
std::string contactPoints = "127.0.0.1"; // defaults to localhost
|
||||
std::optional<uint16_t> port;
|
||||
};
|
||||
|
||||
struct SecureConnectionBundle
|
||||
{
|
||||
std::string bundle; // no meaningful default
|
||||
};
|
||||
|
||||
bool enableLog = false;
|
||||
std::chrono::milliseconds connectionTimeout = std::chrono::milliseconds{10000};
|
||||
std::chrono::milliseconds requestTimeout = std::chrono::milliseconds{0}; // no timeout at all
|
||||
std::variant<ContactPoints, SecureConnectionBundle> connectionInfo = ContactPoints{};
|
||||
uint32_t threads = std::thread::hardware_concurrency();
|
||||
uint32_t maxWriteRequestsOutstanding = 10'000;
|
||||
uint32_t maxReadRequestsOutstanding = 100'000;
|
||||
std::optional<std::string> certificate; // ssl context
|
||||
std::optional<std::string> username;
|
||||
std::optional<std::string> password;
|
||||
|
||||
Settings
|
||||
withContactPoints(std::string_view contactPoints)
|
||||
{
|
||||
auto tmp = *this;
|
||||
tmp.connectionInfo = ContactPoints{std::string{contactPoints}};
|
||||
return tmp;
|
||||
}
|
||||
|
||||
static Settings
|
||||
defaultSettings()
|
||||
{
|
||||
return Settings();
|
||||
}
|
||||
};
|
||||
|
||||
class Cluster : public ManagedObject<CassCluster>
|
||||
{
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
public:
|
||||
Cluster(Settings const& settings);
|
||||
|
||||
private:
|
||||
void
|
||||
setupConnection(Settings const& settings);
|
||||
|
||||
void
|
||||
setupContactPoints(Settings::ContactPoints const& points);
|
||||
|
||||
void
|
||||
setupSecureBundle(Settings::SecureConnectionBundle const& bundle);
|
||||
|
||||
void
|
||||
setupCertificate(Settings const& settings);
|
||||
|
||||
void
|
||||
setupCredentials(Settings const& settings);
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
443
src/backend/cassandra/impl/ExecutionStrategy.h
Normal file
443
src/backend/cassandra/impl/ExecutionStrategy.h
Normal file
@@ -0,0 +1,443 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/AsyncExecutor.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <boost/asio/async_result.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/**
|
||||
* @brief Implements async and sync querying against the cassandra DB with
|
||||
* support for throttling.
|
||||
*
|
||||
* Note: A lot of the code that uses yield is repeated below. This is ok for now
|
||||
* because we are hopefully going to be getting rid of it entirely later on.
|
||||
*/
|
||||
template <typename HandleType = Handle>
|
||||
class DefaultExecutionStrategy
|
||||
{
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
std::uint32_t maxWriteRequestsOutstanding_;
|
||||
std::atomic_uint32_t numWriteRequestsOutstanding_ = 0;
|
||||
|
||||
std::uint32_t maxReadRequestsOutstanding_;
|
||||
std::atomic_uint32_t numReadRequestsOutstanding_ = 0;
|
||||
|
||||
std::mutex throttleMutex_;
|
||||
std::condition_variable throttleCv_;
|
||||
|
||||
std::mutex syncMutex_;
|
||||
std::condition_variable syncCv_;
|
||||
|
||||
boost::asio::io_context ioc_;
|
||||
std::optional<boost::asio::io_service::work> work_;
|
||||
|
||||
std::reference_wrapper<HandleType const> handle_;
|
||||
std::thread thread_;
|
||||
|
||||
public:
|
||||
using ResultOrErrorType = typename HandleType::ResultOrErrorType;
|
||||
using StatementType = typename HandleType::StatementType;
|
||||
using PreparedStatementType = typename HandleType::PreparedStatementType;
|
||||
using FutureType = typename HandleType::FutureType;
|
||||
using FutureWithCallbackType = typename HandleType::FutureWithCallbackType;
|
||||
using ResultType = typename HandleType::ResultType;
|
||||
|
||||
using CompletionTokenType = boost::asio::yield_context;
|
||||
using FunctionType = void(boost::system::error_code);
|
||||
using AsyncResultType = boost::asio::async_result<CompletionTokenType, FunctionType>;
|
||||
using HandlerType = typename AsyncResultType::completion_handler_type;
|
||||
|
||||
DefaultExecutionStrategy(Settings settings, HandleType const& handle)
|
||||
: maxWriteRequestsOutstanding_{settings.maxWriteRequestsOutstanding}
|
||||
, maxReadRequestsOutstanding_{settings.maxReadRequestsOutstanding}
|
||||
, work_{ioc_}
|
||||
, handle_{std::cref(handle)}
|
||||
, thread_{[this]() { ioc_.run(); }}
|
||||
{
|
||||
log_.info() << "Max write requests outstanding is " << maxWriteRequestsOutstanding_
|
||||
<< "; Max read requests outstanding is " << maxReadRequestsOutstanding_;
|
||||
}
|
||||
|
||||
~DefaultExecutionStrategy()
|
||||
{
|
||||
work_.reset();
|
||||
ioc_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wait for all async writes to finish before unblocking
|
||||
*/
|
||||
void
|
||||
sync()
|
||||
{
|
||||
log_.debug() << "Waiting to sync all writes...";
|
||||
std::unique_lock<std::mutex> lck(syncMutex_);
|
||||
syncCv_.wait(lck, [this]() { return finishedAllWriteRequests(); });
|
||||
log_.debug() << "Sync done.";
|
||||
}
|
||||
|
||||
bool
|
||||
isTooBusy() const
|
||||
{
|
||||
return numReadRequestsOutstanding_ >= maxReadRequestsOutstanding_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocking query execution used for writing data
|
||||
*
|
||||
* Retries forever sleeping for 5 milliseconds between attempts.
|
||||
*/
|
||||
ResultOrErrorType
|
||||
writeSync(StatementType const& statement)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (auto res = handle_.get().execute(statement); res)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.warn() << "Cassandra sync write error, retrying: " << res.error();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocking query execution used for writing data
|
||||
*
|
||||
* Retries forever sleeping for 5 milliseconds between attempts.
|
||||
*/
|
||||
template <typename... Args>
|
||||
ResultOrErrorType
|
||||
writeSync(PreparedStatementType const& preparedStatement, Args&&... args)
|
||||
{
|
||||
return writeSync(preparedStatement.bind(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Non-blocking query execution used for writing data
|
||||
*
|
||||
* Retries forever with retry policy specified by @ref AsyncExecutor
|
||||
*
|
||||
* @param prepradeStatement Statement to prepare and execute
|
||||
* @param args Args to bind to the prepared statement
|
||||
* @throw DatabaseTimeout on timeout
|
||||
*/
|
||||
template <typename... Args>
|
||||
void
|
||||
write(PreparedStatementType const& preparedStatement, Args&&... args)
|
||||
{
|
||||
auto statement = preparedStatement.bind(std::forward<Args>(args)...);
|
||||
incrementOutstandingRequestCount();
|
||||
|
||||
// Note: lifetime is controlled by std::shared_from_this internally
|
||||
AsyncExecutor<std::decay_t<decltype(statement)>, HandleType>::run(
|
||||
ioc_, handle_, std::move(statement), [this](auto const&) { decrementOutstandingRequestCount(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Non-blocking batched query execution used for writing data
|
||||
*
|
||||
* Retries forever with retry policy specified by @ref AsyncExecutor.
|
||||
*
|
||||
* @param statements Vector of statements to execute as a batch
|
||||
* @throw DatabaseTimeout on timeout
|
||||
*/
|
||||
void
|
||||
write(std::vector<StatementType>&& statements)
|
||||
{
|
||||
if (statements.empty())
|
||||
return;
|
||||
|
||||
incrementOutstandingRequestCount();
|
||||
|
||||
// Note: lifetime is controlled by std::shared_from_this internally
|
||||
AsyncExecutor<std::decay_t<decltype(statements)>, HandleType>::run(
|
||||
ioc_, handle_, std::move(statements), [this](auto const&) { decrementOutstandingRequestCount(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Coroutine-based query execution used for reading data.
|
||||
*
|
||||
* Retries forever until successful or throws an exception on timeout.
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param prepradeStatement Statement to prepare and execute
|
||||
* @param args Args to bind to the prepared statement
|
||||
* @throw DatabaseTimeout on timeout
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
read(CompletionTokenType token, PreparedStatementType const& preparedStatement, Args&&... args)
|
||||
{
|
||||
return read(token, preparedStatement.bind(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Coroutine-based query execution used for reading data.
|
||||
*
|
||||
* Retries forever until successful or throws an exception on timeout.
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statements Statements to execute in a batch
|
||||
* @throw DatabaseTimeout on timeout
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
read(CompletionTokenType token, std::vector<StatementType> const& statements)
|
||||
{
|
||||
auto handler = HandlerType{token};
|
||||
auto result = AsyncResultType{handler};
|
||||
auto const numStatements = statements.size();
|
||||
|
||||
// todo: perhaps use policy instead
|
||||
while (true)
|
||||
{
|
||||
numReadRequestsOutstanding_ += numStatements;
|
||||
|
||||
auto const future = handle_.get().asyncExecute(statements, [handler](auto&&) mutable {
|
||||
boost::asio::post(boost::asio::get_associated_executor(handler), [handler]() mutable {
|
||||
handler(boost::system::error_code{});
|
||||
});
|
||||
});
|
||||
|
||||
// suspend coroutine until completion handler is called
|
||||
result.get();
|
||||
|
||||
numReadRequestsOutstanding_ -= numStatements;
|
||||
|
||||
// it's safe to call blocking get on future here as we already
|
||||
// waited for the coroutine to resume above.
|
||||
if (auto res = future.get(); res)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Failed batch read in coroutine: " << res.error();
|
||||
throwErrorIfNeeded(res.error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Coroutine-based query execution used for reading data.
|
||||
*
|
||||
* Retries forever until successful or throws an exception on timeout.
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statement Statement to execute
|
||||
* @throw DatabaseTimeout on timeout
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
read(CompletionTokenType token, StatementType const& statement)
|
||||
{
|
||||
auto handler = HandlerType{token};
|
||||
auto result = AsyncResultType{handler};
|
||||
|
||||
// todo: perhaps use policy instead
|
||||
while (true)
|
||||
{
|
||||
++numReadRequestsOutstanding_;
|
||||
|
||||
auto const future = handle_.get().asyncExecute(statement, [handler](auto const&) mutable {
|
||||
boost::asio::post(boost::asio::get_associated_executor(handler), [handler]() mutable {
|
||||
handler(boost::system::error_code{});
|
||||
});
|
||||
});
|
||||
|
||||
// suspend coroutine until completion handler is called
|
||||
result.get();
|
||||
|
||||
--numReadRequestsOutstanding_;
|
||||
|
||||
// it's safe to call blocking get on future here as we already
|
||||
// waited for the coroutine to resume above.
|
||||
if (auto res = future.get(); res)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_.error() << "Failed read in coroutine: " << res.error();
|
||||
throwErrorIfNeeded(res.error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Coroutine-based query execution used for reading data.
|
||||
*
|
||||
* Attempts to execute each statement. On any error the whole vector will be
|
||||
* discarded and exception will be thrown.
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statements Statements to execute
|
||||
* @throw DatabaseTimeout on db error
|
||||
* @return Vector of results
|
||||
*/
|
||||
std::vector<ResultType>
|
||||
readEach(CompletionTokenType token, std::vector<StatementType> const& statements)
|
||||
{
|
||||
auto handler = HandlerType{token};
|
||||
auto result = AsyncResultType{handler};
|
||||
|
||||
std::atomic_bool hadError = false;
|
||||
std::atomic_int numOutstanding = statements.size();
|
||||
numReadRequestsOutstanding_ += statements.size();
|
||||
|
||||
auto futures = std::vector<FutureWithCallbackType>{};
|
||||
futures.reserve(numOutstanding);
|
||||
|
||||
// used as the handler for each async statement individually
|
||||
auto executionHandler = [handler, &hadError, &numOutstanding](auto const& res) mutable {
|
||||
if (not res)
|
||||
hadError = true;
|
||||
|
||||
// when all async operations complete unblock the result
|
||||
if (--numOutstanding == 0)
|
||||
boost::asio::post(boost::asio::get_associated_executor(handler), [handler]() mutable {
|
||||
handler(boost::system::error_code{});
|
||||
});
|
||||
};
|
||||
|
||||
std::transform(
|
||||
std::cbegin(statements),
|
||||
std::cend(statements),
|
||||
std::back_inserter(futures),
|
||||
[this, &executionHandler](auto const& statement) {
|
||||
return handle_.get().asyncExecute(statement, executionHandler);
|
||||
});
|
||||
|
||||
// suspend coroutine until completion handler is called
|
||||
result.get();
|
||||
|
||||
numReadRequestsOutstanding_ -= statements.size();
|
||||
|
||||
if (hadError)
|
||||
throw DatabaseTimeout{};
|
||||
|
||||
std::vector<ResultType> results;
|
||||
results.reserve(futures.size());
|
||||
|
||||
// it's safe to call blocking get on futures here as we already
|
||||
// waited for the coroutine to resume above.
|
||||
std::transform(
|
||||
std::make_move_iterator(std::begin(futures)),
|
||||
std::make_move_iterator(std::end(futures)),
|
||||
std::back_inserter(results),
|
||||
[](auto&& future) {
|
||||
auto entry = future.get();
|
||||
auto&& res = entry.value();
|
||||
return std::move(res);
|
||||
});
|
||||
|
||||
assert(futures.size() == statements.size());
|
||||
assert(results.size() == statements.size());
|
||||
return results;
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
incrementOutstandingRequestCount()
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lck(throttleMutex_);
|
||||
if (!canAddWriteRequest())
|
||||
{
|
||||
log_.trace() << "Max outstanding requests reached. "
|
||||
<< "Waiting for other requests to finish";
|
||||
throttleCv_.wait(lck, [this]() { return canAddWriteRequest(); });
|
||||
}
|
||||
}
|
||||
++numWriteRequestsOutstanding_;
|
||||
}
|
||||
|
||||
void
|
||||
decrementOutstandingRequestCount()
|
||||
{
|
||||
// sanity check
|
||||
if (numWriteRequestsOutstanding_ == 0)
|
||||
{
|
||||
assert(false);
|
||||
throw std::runtime_error("decrementing num outstanding below 0");
|
||||
}
|
||||
size_t cur = (--numWriteRequestsOutstanding_);
|
||||
{
|
||||
// mutex lock required to prevent race condition around spurious
|
||||
// wakeup
|
||||
std::lock_guard lck(throttleMutex_);
|
||||
throttleCv_.notify_one();
|
||||
}
|
||||
if (cur == 0)
|
||||
{
|
||||
// mutex lock required to prevent race condition around spurious
|
||||
// wakeup
|
||||
std::lock_guard lck(syncMutex_);
|
||||
syncCv_.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
canAddWriteRequest() const
|
||||
{
|
||||
return numWriteRequestsOutstanding_ < maxWriteRequestsOutstanding_;
|
||||
}
|
||||
|
||||
bool
|
||||
finishedAllWriteRequests() const
|
||||
{
|
||||
return numWriteRequestsOutstanding_ == 0;
|
||||
}
|
||||
|
||||
void
|
||||
throwErrorIfNeeded(CassandraError err) const
|
||||
{
|
||||
if (err.isTimeout())
|
||||
throw DatabaseTimeout();
|
||||
|
||||
if (err.isInvalidQuery())
|
||||
throw std::runtime_error("Invalid query");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
102
src/backend/cassandra/impl/Future.cpp
Normal file
102
src/backend/cassandra/impl/Future.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/Error.h>
|
||||
#include <backend/cassandra/impl/Future.h>
|
||||
#include <backend/cassandra/impl/Result.h>
|
||||
|
||||
#include <exception>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
static constexpr auto futureDeleter = [](CassFuture* ptr) { cass_future_free(ptr); };
|
||||
} // namespace
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/* implicit */ Future::Future(CassFuture* ptr) : ManagedObject{ptr, futureDeleter}
|
||||
{
|
||||
}
|
||||
|
||||
MaybeError
|
||||
Future::await() const
|
||||
{
|
||||
if (auto const rc = cass_future_error_code(*this); rc)
|
||||
{
|
||||
auto errMsg = [this](std::string const& label) {
|
||||
char const* message;
|
||||
std::size_t len;
|
||||
cass_future_error_message(*this, &message, &len);
|
||||
return label + ": " + std::string{message, len};
|
||||
}(cass_error_desc(rc));
|
||||
return Error{CassandraError{errMsg, rc}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ResultOrError
|
||||
Future::get() const
|
||||
{
|
||||
if (auto const rc = cass_future_error_code(*this); rc)
|
||||
{
|
||||
auto const errMsg = [this](std::string const& label) {
|
||||
char const* message;
|
||||
std::size_t len;
|
||||
cass_future_error_message(*this, &message, &len);
|
||||
return label + ": " + std::string{message, len};
|
||||
}("future::get()");
|
||||
return Error{CassandraError{errMsg, rc}};
|
||||
}
|
||||
else
|
||||
{
|
||||
return Result{cass_future_get_result(*this)};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
invokeHelper(CassFuture* ptr, void* cbPtr)
|
||||
{
|
||||
// Note: can't use Future{ptr}.get() because double free will occur :/
|
||||
auto* cb = static_cast<FutureWithCallback::fn_t*>(cbPtr);
|
||||
if (auto const rc = cass_future_error_code(ptr); rc)
|
||||
{
|
||||
auto const errMsg = [&ptr](std::string const& label) {
|
||||
char const* message;
|
||||
std::size_t len;
|
||||
cass_future_error_message(ptr, &message, &len);
|
||||
return label + ": " + std::string{message, len};
|
||||
}("invokeHelper");
|
||||
(*cb)(Error{CassandraError{errMsg, rc}});
|
||||
}
|
||||
else
|
||||
{
|
||||
(*cb)(Result{cass_future_get_result(ptr)});
|
||||
}
|
||||
}
|
||||
|
||||
/* implicit */ FutureWithCallback::FutureWithCallback(CassFuture* ptr, fn_t&& cb)
|
||||
: Future{ptr}, cb_{std::make_unique<fn_t>(std::move(cb))}
|
||||
{
|
||||
// Instead of passing `this` as the userdata void*, we pass the address of
|
||||
// the callback itself which will survive std::move of the
|
||||
// FutureWithCallback parent. Not ideal but I have no better solution atm.
|
||||
cass_future_set_callback(*this, &invokeHelper, cb_.get());
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
58
src/backend/cassandra/impl/Future.h
Normal file
58
src/backend/cassandra/impl/Future.h
Normal file
@@ -0,0 +1,58 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
struct Future : public ManagedObject<CassFuture>
|
||||
{
|
||||
/* implicit */ Future(CassFuture* ptr);
|
||||
|
||||
MaybeError
|
||||
await() const;
|
||||
|
||||
ResultOrError
|
||||
get() const;
|
||||
};
|
||||
|
||||
void
|
||||
invokeHelper(CassFuture* ptr, void* self);
|
||||
|
||||
class FutureWithCallback : public Future
|
||||
{
|
||||
public:
|
||||
using fn_t = std::function<void(ResultOrError)>;
|
||||
using fn_ptr_t = std::unique_ptr<fn_t>;
|
||||
|
||||
/* implicit */ FutureWithCallback(CassFuture* ptr, fn_t&& cb);
|
||||
FutureWithCallback(FutureWithCallback const&) = delete;
|
||||
FutureWithCallback(FutureWithCallback&&) = default;
|
||||
|
||||
private:
|
||||
/*! Wrapped in a unique_ptr so it can survive std::move :/ */
|
||||
fn_ptr_t cb_;
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
47
src/backend/cassandra/impl/ManagedObject.h
Normal file
47
src/backend/cassandra/impl/ManagedObject.h
Normal file
@@ -0,0 +1,47 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
template <typename Managed>
|
||||
class ManagedObject
|
||||
{
|
||||
protected:
|
||||
std::unique_ptr<Managed, void (*)(Managed*)> ptr_;
|
||||
|
||||
public:
|
||||
template <typename deleterCallable>
|
||||
ManagedObject(Managed* rawPtr, deleterCallable deleter) : ptr_{rawPtr, deleter}
|
||||
{
|
||||
if (rawPtr == nullptr)
|
||||
throw std::runtime_error("Could not create DB object - got nullptr");
|
||||
}
|
||||
ManagedObject(ManagedObject&&) = default;
|
||||
|
||||
operator Managed* const() const
|
||||
{
|
||||
return ptr_.get();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
69
src/backend/cassandra/impl/Result.cpp
Normal file
69
src/backend/cassandra/impl/Result.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/impl/Result.h>
|
||||
|
||||
namespace {
|
||||
static constexpr auto resultDeleter = [](CassResult const* ptr) { cass_result_free(ptr); };
|
||||
static constexpr auto resultIteratorDeleter = [](CassIterator* ptr) { cass_iterator_free(ptr); };
|
||||
} // namespace
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/* implicit */ Result::Result(CassResult const* ptr) : ManagedObject{ptr, resultDeleter}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t
|
||||
Result::numRows() const
|
||||
{
|
||||
return cass_result_row_count(*this);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
Result::hasRows() const
|
||||
{
|
||||
return numRows() > 0;
|
||||
}
|
||||
|
||||
/* implicit */ ResultIterator::ResultIterator(CassIterator* ptr)
|
||||
: ManagedObject{ptr, resultIteratorDeleter}, hasMore_{cass_iterator_next(ptr)}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] ResultIterator
|
||||
ResultIterator::fromResult(Result const& result)
|
||||
{
|
||||
return {cass_iterator_from_result(result)};
|
||||
}
|
||||
|
||||
[[maybe_unused]] bool
|
||||
ResultIterator::moveForward()
|
||||
{
|
||||
hasMore_ = cass_iterator_next(*this);
|
||||
return hasMore_;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
ResultIterator::hasMore() const
|
||||
{
|
||||
return hasMore_;
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
257
src/backend/cassandra/impl/Result.h
Normal file
257
src/backend/cassandra/impl/Result.h
Normal file
@@ -0,0 +1,257 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
#include <backend/cassandra/impl/Tuple.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <compare>
|
||||
#include <iterator>
|
||||
#include <tuple>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
template <typename>
|
||||
static constexpr bool unsupported_v = false;
|
||||
|
||||
template <typename Type>
|
||||
inline Type
|
||||
extractColumn(CassRow const* row, std::size_t idx)
|
||||
{
|
||||
using std::to_string;
|
||||
Type output;
|
||||
|
||||
auto throwErrorIfNeeded = [](CassError rc, std::string_view label) {
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
auto const tag = '[' + std::string{label} + ']';
|
||||
throw std::logic_error(tag + ": " + cass_error_desc(rc));
|
||||
}
|
||||
};
|
||||
|
||||
using decayed_t = std::decay_t<Type>;
|
||||
using uint_tuple_t = std::tuple<uint32_t, uint32_t>;
|
||||
using uchar_vector_t = std::vector<unsigned char>;
|
||||
|
||||
if constexpr (std::is_same_v<decayed_t, ripple::uint256>)
|
||||
{
|
||||
cass_byte_t const* buf;
|
||||
std::size_t bufSize;
|
||||
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
|
||||
throwErrorIfNeeded(rc, "Extract ripple::uint256");
|
||||
output = ripple::uint256::fromVoid(buf);
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, ripple::AccountID>)
|
||||
{
|
||||
cass_byte_t const* buf;
|
||||
std::size_t bufSize;
|
||||
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
|
||||
throwErrorIfNeeded(rc, "Extract ripple::AccountID");
|
||||
output = ripple::AccountID::fromVoid(buf);
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, uchar_vector_t>)
|
||||
{
|
||||
cass_byte_t const* buf;
|
||||
std::size_t bufSize;
|
||||
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
|
||||
throwErrorIfNeeded(rc, "Extract vector<unsigned char>");
|
||||
output = uchar_vector_t{buf, buf + bufSize};
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, uint_tuple_t>)
|
||||
{
|
||||
auto const* tuple = cass_row_get_column(row, idx);
|
||||
output = TupleIterator::fromTuple(tuple).extract<uint32_t, uint32_t>();
|
||||
}
|
||||
else if constexpr (std::is_convertible_v<decayed_t, std::string>)
|
||||
{
|
||||
char const* value;
|
||||
std::size_t len;
|
||||
auto const rc = cass_value_get_string(cass_row_get_column(row, idx), &value, &len);
|
||||
throwErrorIfNeeded(rc, "Extract string");
|
||||
output = std::string{value, len};
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, bool>)
|
||||
{
|
||||
cass_bool_t flag;
|
||||
auto const rc = cass_value_get_bool(cass_row_get_column(row, idx), &flag);
|
||||
throwErrorIfNeeded(rc, "Extract bool");
|
||||
output = flag ? true : false;
|
||||
}
|
||||
// clio only uses bigint (int64_t) so we convert any incoming type
|
||||
else if constexpr (std::is_convertible_v<decayed_t, int64_t>)
|
||||
{
|
||||
int64_t out;
|
||||
auto const rc = cass_value_get_int64(cass_row_get_column(row, idx), &out);
|
||||
throwErrorIfNeeded(rc, "Extract int64");
|
||||
output = static_cast<decayed_t>(out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// type not supported for extraction
|
||||
static_assert(unsupported_v<decayed_t>);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
struct Result : public ManagedObject<CassResult const>
|
||||
{
|
||||
/* implicit */ Result(CassResult const* ptr);
|
||||
|
||||
[[nodiscard]] std::size_t
|
||||
numRows() const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasRows() const;
|
||||
|
||||
template <typename... RowTypes>
|
||||
std::optional<std::tuple<RowTypes...>>
|
||||
get() const requires(std::tuple_size<std::tuple<RowTypes...>>{} > 1)
|
||||
{
|
||||
// row managed internally by cassandra driver, hence no ManagedObject.
|
||||
auto const* row = cass_result_first_row(*this);
|
||||
if (row == nullptr)
|
||||
return std::nullopt;
|
||||
|
||||
std::size_t idx = 0;
|
||||
auto advanceId = [&idx]() { return idx++; };
|
||||
|
||||
return std::make_optional<std::tuple<RowTypes...>>({extractColumn<RowTypes>(row, advanceId())...});
|
||||
}
|
||||
|
||||
template <typename RowType>
|
||||
std::optional<RowType>
|
||||
get() const
|
||||
{
|
||||
// row managed internally by cassandra driver, hence no ManagedObject.
|
||||
auto const* row = cass_result_first_row(*this);
|
||||
if (row == nullptr)
|
||||
return std::nullopt;
|
||||
return std::make_optional<RowType>(extractColumn<RowType>(row, 0));
|
||||
}
|
||||
};
|
||||
|
||||
class ResultIterator : public ManagedObject<CassIterator>
|
||||
{
|
||||
bool hasMore_ = false;
|
||||
|
||||
public:
|
||||
/* implicit */ ResultIterator(CassIterator* ptr);
|
||||
|
||||
[[nodiscard]] static ResultIterator
|
||||
fromResult(Result const& result);
|
||||
|
||||
[[maybe_unused]] bool
|
||||
moveForward();
|
||||
|
||||
[[nodiscard]] bool
|
||||
hasMore() const;
|
||||
|
||||
template <typename... RowTypes>
|
||||
std::tuple<RowTypes...>
|
||||
extractCurrentRow() const
|
||||
{
|
||||
// note: row is invalidated on each iteration.
|
||||
// managed internally by cassandra driver, hence no ManagedObject.
|
||||
auto const* row = cass_iterator_get_row(*this);
|
||||
|
||||
std::size_t idx = 0;
|
||||
auto advanceId = [&idx]() { return idx++; };
|
||||
|
||||
return {extractColumn<RowTypes>(row, advanceId())...};
|
||||
}
|
||||
};
|
||||
|
||||
template <typename... Types>
|
||||
class ResultExtractor
|
||||
{
|
||||
std::reference_wrapper<Result const> ref_;
|
||||
|
||||
public:
|
||||
struct Sentinel
|
||||
{
|
||||
};
|
||||
|
||||
struct Iterator
|
||||
{
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using difference_type = std::size_t; // rows count
|
||||
using value_type = std::tuple<Types...>;
|
||||
|
||||
/* implicit */ Iterator(ResultIterator iterator) : iterator_{std::move(iterator)}
|
||||
{
|
||||
}
|
||||
|
||||
Iterator(Iterator const&) = delete;
|
||||
Iterator&
|
||||
operator=(Iterator const&) = delete;
|
||||
|
||||
value_type
|
||||
operator*() const
|
||||
{
|
||||
return iterator_.extractCurrentRow<Types...>();
|
||||
}
|
||||
|
||||
value_type
|
||||
operator->()
|
||||
{
|
||||
return iterator_.extractCurrentRow<Types...>();
|
||||
}
|
||||
|
||||
Iterator&
|
||||
operator++()
|
||||
{
|
||||
iterator_.moveForward();
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(Sentinel const&) const
|
||||
{
|
||||
return not iterator_.hasMore();
|
||||
}
|
||||
|
||||
private:
|
||||
ResultIterator iterator_;
|
||||
};
|
||||
|
||||
ResultExtractor(Result const& result) : ref_{std::cref(result)}
|
||||
{
|
||||
}
|
||||
|
||||
Iterator
|
||||
begin()
|
||||
{
|
||||
return ResultIterator::fromResult(ref_);
|
||||
}
|
||||
|
||||
Sentinel
|
||||
end()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
94
src/backend/cassandra/impl/RetryPolicy.h
Normal file
94
src/backend/cassandra/impl/RetryPolicy.h
Normal file
@@ -0,0 +1,94 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Handle.h>
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <log/Logger.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/**
|
||||
* @brief A retry policy that employs exponential backoff
|
||||
*/
|
||||
class ExponentialBackoffRetryPolicy
|
||||
{
|
||||
clio::Logger log_{"Backend"};
|
||||
|
||||
boost::asio::steady_timer timer_;
|
||||
uint32_t attempt_ = 0u;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new retry policy instance with the io_context provided
|
||||
*/
|
||||
ExponentialBackoffRetryPolicy(boost::asio::io_context& ioc) : timer_{ioc}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes next retry delay and returns true unconditionally
|
||||
*
|
||||
* @param err The cassandra error that triggered the retry
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
shouldRetry([[maybe_unused]] CassandraError err)
|
||||
{
|
||||
auto const delay = calculateDelay(attempt_);
|
||||
log_.error() << "Cassandra write error: " << err << ", current retries " << attempt_ << ", retrying in "
|
||||
<< delay.count() << " milliseconds";
|
||||
|
||||
return true; // keep retrying forever
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Schedules next retry
|
||||
*
|
||||
* @param fn The callable to execute
|
||||
*/
|
||||
template <typename Fn>
|
||||
void
|
||||
retry(Fn&& fn)
|
||||
{
|
||||
timer_.expires_after(calculateDelay(attempt_++));
|
||||
timer_.async_wait([fn = std::forward<Fn>(fn)]([[maybe_unused]] const auto& err) {
|
||||
// todo: deal with cancellation (thru err)
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the wait time before attempting another retry
|
||||
*/
|
||||
std::chrono::milliseconds
|
||||
calculateDelay(uint32_t attempt)
|
||||
{
|
||||
return std::chrono::milliseconds{lround(std::pow(2, std::min(10u, attempt)))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
38
src/backend/cassandra/impl/Session.h
Normal file
38
src/backend/cassandra/impl/Session.h
Normal file
@@ -0,0 +1,38 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
class Session : public ManagedObject<CassSession>
|
||||
{
|
||||
static constexpr auto deleter = [](CassSession* ptr) { cass_session_free(ptr); };
|
||||
|
||||
public:
|
||||
Session() : ManagedObject{cass_session_new(), deleter}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
37
src/backend/cassandra/impl/SslContext.cpp
Normal file
37
src/backend/cassandra/impl/SslContext.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/impl/SslContext.h>
|
||||
|
||||
namespace {
|
||||
static constexpr auto contextDeleter = [](CassSsl* ptr) { cass_ssl_free(ptr); };
|
||||
} // namespace
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
SslContext::SslContext(std::string const& certificate) : ManagedObject{cass_ssl_new(), contextDeleter}
|
||||
{
|
||||
cass_ssl_set_verify_flags(*this, CASS_SSL_VERIFY_NONE);
|
||||
if (auto const rc = cass_ssl_add_trusted_cert(*this, certificate.c_str()); rc != CASS_OK)
|
||||
{
|
||||
throw std::runtime_error(std::string{"Error setting Cassandra SSL Context: "} + cass_error_desc(rc));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
35
src/backend/cassandra/impl/SslContext.h
Normal file
35
src/backend/cassandra/impl/SslContext.h
Normal file
@@ -0,0 +1,35 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
struct SslContext : public ManagedObject<CassSsl>
|
||||
{
|
||||
explicit SslContext(std::string const& certificate);
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
164
src/backend/cassandra/impl/Statement.h
Normal file
164
src/backend/cassandra/impl/Statement.h
Normal file
@@ -0,0 +1,164 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/Types.h>
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
#include <backend/cassandra/impl/Tuple.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/STAccount.h>
|
||||
#include <cassandra.h>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <compare>
|
||||
#include <iterator>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
class Statement : public ManagedObject<CassStatement>
|
||||
{
|
||||
static constexpr auto deleter = [](CassStatement* ptr) { cass_statement_free(ptr); };
|
||||
|
||||
template <typename>
|
||||
static constexpr bool unsupported_v = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new statement with optionally provided arguments
|
||||
*
|
||||
* Note: it's up to the user to make sure the bound parameters match
|
||||
* the format of the query (e.g. amount of '?' matches count of args).
|
||||
*/
|
||||
template <typename... Args>
|
||||
explicit Statement(std::string_view query, Args&&... args)
|
||||
: ManagedObject{cass_statement_new(query.data(), sizeof...(args)), deleter}
|
||||
{
|
||||
cass_statement_set_consistency(*this, CASS_CONSISTENCY_QUORUM);
|
||||
cass_statement_set_is_idempotent(*this, cass_true);
|
||||
bind<Args...>(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/* implicit */ Statement(CassStatement* ptr) : ManagedObject{ptr, deleter}
|
||||
{
|
||||
cass_statement_set_consistency(*this, CASS_CONSISTENCY_QUORUM);
|
||||
cass_statement_set_is_idempotent(*this, cass_true);
|
||||
}
|
||||
|
||||
Statement(Statement&&) = default;
|
||||
|
||||
template <typename... Args>
|
||||
void
|
||||
bind(Args&&... args) const
|
||||
{
|
||||
std::size_t idx = 0;
|
||||
(this->bindAt<Args>(idx++, std::forward<Args>(args)), ...);
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
void
|
||||
bindAt(std::size_t const idx, Type&& value) const
|
||||
{
|
||||
using std::to_string;
|
||||
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
|
||||
if (rc != CASS_OK)
|
||||
throw std::logic_error(fmt::format("[{}] at idx {}: {}", label, idx, cass_error_desc(rc)));
|
||||
};
|
||||
|
||||
auto bindBytes = [this, idx](auto const* data, size_t size) {
|
||||
return cass_statement_bind_bytes(*this, idx, static_cast<cass_byte_t const*>(data), size);
|
||||
};
|
||||
|
||||
using decayed_t = std::decay_t<Type>;
|
||||
using uchar_vec_t = std::vector<unsigned char>;
|
||||
using uint_tuple_t = std::tuple<uint32_t, uint32_t>;
|
||||
|
||||
if constexpr (std::is_same_v<decayed_t, ripple::uint256>)
|
||||
{
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::uint256");
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, ripple::AccountID>)
|
||||
{
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, uchar_vec_t>)
|
||||
{
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind vector<unsigned char>");
|
||||
}
|
||||
else if constexpr (std::is_convertible_v<decayed_t, std::string>)
|
||||
{
|
||||
// reinterpret_cast is needed here :'(
|
||||
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind string (as bytes)");
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, uint_tuple_t>)
|
||||
{
|
||||
auto const rc = cass_statement_bind_tuple(*this, idx, Tuple{std::move(value)});
|
||||
throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32>");
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, bool>)
|
||||
{
|
||||
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);
|
||||
throwErrorIfNeeded(rc, "Bind bool");
|
||||
}
|
||||
else if constexpr (std::is_same_v<decayed_t, Limit>)
|
||||
{
|
||||
auto const rc = cass_statement_bind_int32(*this, idx, value.limit);
|
||||
throwErrorIfNeeded(rc, "Bind limit (int32)");
|
||||
}
|
||||
// clio only uses bigint (int64_t) so we convert any incoming type
|
||||
else if constexpr (std::is_convertible_v<decayed_t, int64_t>)
|
||||
{
|
||||
auto const rc = cass_statement_bind_int64(*this, idx, value);
|
||||
throwErrorIfNeeded(rc, "Bind int64");
|
||||
}
|
||||
else
|
||||
{
|
||||
// type not supported for binding
|
||||
static_assert(unsupported_v<decayed_t>);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class PreparedStatement : public ManagedObject<CassPrepared const>
|
||||
{
|
||||
static constexpr auto deleter = [](CassPrepared const* ptr) { cass_prepared_free(ptr); };
|
||||
|
||||
public:
|
||||
/* implicit */ PreparedStatement(CassPrepared const* ptr) : ManagedObject{ptr, deleter}
|
||||
{
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
Statement
|
||||
bind(Args&&... args) const
|
||||
{
|
||||
Statement statement = cass_prepared_bind(*this);
|
||||
statement.bind<Args...>(std::forward<Args>(args)...);
|
||||
return statement;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
43
src/backend/cassandra/impl/Tuple.cpp
Normal file
43
src/backend/cassandra/impl/Tuple.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <backend/cassandra/impl/Tuple.h>
|
||||
|
||||
namespace {
|
||||
static constexpr auto tupleDeleter = [](CassTuple* ptr) { cass_tuple_free(ptr); };
|
||||
static constexpr auto tupleIteratorDeleter = [](CassIterator* ptr) { cass_iterator_free(ptr); };
|
||||
} // namespace
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
/* implicit */ Tuple::Tuple(CassTuple* ptr) : ManagedObject{ptr, tupleDeleter}
|
||||
{
|
||||
}
|
||||
|
||||
/* implicit */ TupleIterator::TupleIterator(CassIterator* ptr) : ManagedObject{ptr, tupleIteratorDeleter}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] TupleIterator
|
||||
TupleIterator::fromTuple(CassValue const* value)
|
||||
{
|
||||
return {cass_iterator_from_tuple(value)};
|
||||
}
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
149
src/backend/cassandra/impl/Tuple.h
Normal file
149
src/backend/cassandra/impl/Tuple.h
Normal file
@@ -0,0 +1,149 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/cassandra/impl/ManagedObject.h>
|
||||
|
||||
#include <cassandra.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
|
||||
namespace Backend::Cassandra::detail {
|
||||
|
||||
class Tuple : public ManagedObject<CassTuple>
|
||||
{
|
||||
static constexpr auto deleter = [](CassTuple* ptr) { cass_tuple_free(ptr); };
|
||||
|
||||
template <typename>
|
||||
static constexpr bool unsupported_v = false;
|
||||
|
||||
public:
|
||||
/* implicit */ Tuple(CassTuple* ptr);
|
||||
|
||||
template <typename... Types>
|
||||
explicit Tuple(std::tuple<Types...>&& value)
|
||||
: ManagedObject{cass_tuple_new(std::tuple_size<std::tuple<Types...>>{}), deleter}
|
||||
{
|
||||
std::apply(std::bind_front(&Tuple::bind<Types...>, this), std::move(value));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void
|
||||
bind(Args&&... args) const
|
||||
{
|
||||
std::size_t idx = 0;
|
||||
(this->bindAt<Args>(idx++, std::forward<Args>(args)), ...);
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
void
|
||||
bindAt(std::size_t const idx, Type&& value) const
|
||||
{
|
||||
using std::to_string;
|
||||
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
auto const tag = '[' + std::string{label} + ']';
|
||||
throw std::logic_error(tag + " at idx " + to_string(idx) + ": " + cass_error_desc(rc));
|
||||
}
|
||||
};
|
||||
|
||||
using decayed_t = std::decay_t<Type>;
|
||||
|
||||
if constexpr (std::is_same_v<decayed_t, bool>)
|
||||
{
|
||||
auto const rc = cass_tuple_set_bool(*this, idx, value ? cass_true : cass_false);
|
||||
throwErrorIfNeeded(rc, "Bind bool");
|
||||
}
|
||||
// clio only uses bigint (int64_t) so we convert any incoming type
|
||||
else if constexpr (std::is_convertible_v<decayed_t, int64_t>)
|
||||
{
|
||||
auto const rc = cass_tuple_set_int64(*this, idx, value);
|
||||
throwErrorIfNeeded(rc, "Bind int64");
|
||||
}
|
||||
else
|
||||
{
|
||||
// type not supported for binding
|
||||
static_assert(unsupported_v<decayed_t>);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TupleIterator : public ManagedObject<CassIterator>
|
||||
{
|
||||
template <typename>
|
||||
static constexpr bool unsupported_v = false;
|
||||
|
||||
public:
|
||||
/* implicit */ TupleIterator(CassIterator* ptr);
|
||||
|
||||
[[nodiscard]] static TupleIterator
|
||||
fromTuple(CassValue const* value);
|
||||
|
||||
template <typename... Types>
|
||||
[[nodiscard]] std::tuple<Types...>
|
||||
extract() const
|
||||
{
|
||||
return {extractNext<Types>()...};
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Type>
|
||||
Type
|
||||
extractNext() const
|
||||
{
|
||||
using std::to_string;
|
||||
Type output;
|
||||
|
||||
if (not cass_iterator_next(*this))
|
||||
throw std::logic_error("Could not extract next value from tuple iterator");
|
||||
|
||||
auto throwErrorIfNeeded = [](CassError rc, std::string_view label) {
|
||||
if (rc != CASS_OK)
|
||||
{
|
||||
auto const tag = '[' + std::string{label} + ']';
|
||||
throw std::logic_error(tag + ": " + cass_error_desc(rc));
|
||||
}
|
||||
};
|
||||
|
||||
using decayed_t = std::decay_t<Type>;
|
||||
|
||||
// clio only uses bigint (int64_t) so we convert any incoming type
|
||||
if constexpr (std::is_convertible_v<decayed_t, int64_t>)
|
||||
{
|
||||
int64_t out;
|
||||
auto const rc = cass_value_get_int64(cass_iterator_get_value(*this), &out);
|
||||
throwErrorIfNeeded(rc, "Extract int64 from tuple");
|
||||
output = static_cast<decayed_t>(out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// type not supported for extraction
|
||||
static_assert(unsupported_v<decayed_t>);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Backend::Cassandra::detail
|
||||
@@ -1,6 +1,25 @@
|
||||
#include <config/Config.h>
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <fstream>
|
||||
|
||||
namespace clio {
|
||||
@@ -43,8 +62,7 @@ Config::lookup(key_type key) const
|
||||
if (not hasBrokenPath)
|
||||
{
|
||||
if (not cur.get().is_object())
|
||||
throw detail::StoreException(
|
||||
"Not an object at '" + subkey + "'");
|
||||
throw detail::StoreException("Not an object at '" + subkey + "'");
|
||||
if (not cur.get().as_object().contains(section))
|
||||
hasBrokenPath = true;
|
||||
else
|
||||
@@ -72,11 +90,9 @@ Config::maybeArray(key_type key) const
|
||||
array_type out;
|
||||
out.reserve(arr.size());
|
||||
|
||||
std::transform(
|
||||
std::begin(arr),
|
||||
std::end(arr),
|
||||
std::back_inserter(out),
|
||||
[](auto&& element) { return Config{std::move(element)}; });
|
||||
std::transform(std::begin(arr), std::end(arr), std::back_inserter(out), [](auto&& element) {
|
||||
return Config{std::move(element)};
|
||||
});
|
||||
return std::make_optional<array_type>(std::move(out));
|
||||
}
|
||||
}
|
||||
@@ -137,10 +153,7 @@ Config::array() const
|
||||
out.reserve(arr.size());
|
||||
|
||||
std::transform(
|
||||
std::cbegin(arr),
|
||||
std::cend(arr),
|
||||
std::back_inserter(out),
|
||||
[](auto const& element) { return Config{element}; });
|
||||
std::cbegin(arr), std::cend(arr), std::back_inserter(out), [](auto const& element) { return Config{element}; });
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -161,11 +174,9 @@ ConfigReader::open(std::filesystem::path path)
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Could not read configuration file from '"
|
||||
<< path.string() << "': " << e.what();
|
||||
LogService::error() << "Could not read configuration file from '" << path.string() << "': " << e.what();
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "Using empty default configuration";
|
||||
return Config{};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef RIPPLE_APP_CONFIG_H_INCLUDED
|
||||
#define RIPPLE_APP_CONFIG_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <config/detail/Helpers.h>
|
||||
|
||||
@@ -25,9 +43,7 @@ class Config final
|
||||
public:
|
||||
using key_type = std::string; /*! The type of key used */
|
||||
using array_type = std::vector<Config>; /*! The type of array used */
|
||||
using write_cursor_type = std::pair<
|
||||
std::optional<std::reference_wrapper<boost::json::value>>,
|
||||
key_type>;
|
||||
using write_cursor_type = std::pair<std::optional<std::reference_wrapper<boost::json::value>>, key_type>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new Config object.
|
||||
@@ -83,8 +99,7 @@ public:
|
||||
{
|
||||
auto maybe_element = lookup(key);
|
||||
if (maybe_element)
|
||||
return std::make_optional<Result>(
|
||||
checkedAs<Result>(key, *maybe_element));
|
||||
return std::make_optional<Result>(checkedAs<Result>(key, *maybe_element));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -331,6 +346,8 @@ private:
|
||||
[[nodiscard]] Return
|
||||
checkedAs(key_type key, boost::json::value const& value) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
auto has_error = false;
|
||||
if constexpr (std::is_same_v<Return, bool>)
|
||||
{
|
||||
@@ -344,12 +361,10 @@ private:
|
||||
}
|
||||
else if constexpr (std::is_same_v<Return, double>)
|
||||
{
|
||||
if (not value.is_double())
|
||||
if (not value.is_number())
|
||||
has_error = true;
|
||||
}
|
||||
else if constexpr (
|
||||
std::is_convertible_v<Return, uint64_t> ||
|
||||
std::is_convertible_v<Return, int64_t>)
|
||||
else if constexpr (std::is_convertible_v<Return, uint64_t> || std::is_convertible_v<Return, int64_t>)
|
||||
{
|
||||
if (not value.is_int64() && not value.is_uint64())
|
||||
has_error = true;
|
||||
@@ -357,11 +372,10 @@ private:
|
||||
|
||||
if (has_error)
|
||||
throw std::runtime_error(
|
||||
"Type for key '" + key + "' is '" +
|
||||
std::string{to_string(value.kind())} +
|
||||
"' in JSON but requested '" + detail::typeName<Return>() + "'");
|
||||
"Type for key '" + key + "' is '" + std::string{to_string(value.kind())} + "' in JSON but requested '" +
|
||||
detail::typeName<Return>() + "'");
|
||||
|
||||
return boost::json::value_to<Return>(value);
|
||||
return value_to<Return>(value);
|
||||
}
|
||||
|
||||
std::optional<boost::json::value>
|
||||
@@ -385,5 +399,3 @@ public:
|
||||
};
|
||||
|
||||
} // namespace clio
|
||||
|
||||
#endif // RIPPLE_APP_CONFIG_H_INCLUDED
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef RIPPLE_APP_CONFIG_DETAIL_H_INCLUDED
|
||||
#define RIPPLE_APP_CONFIG_DETAIL_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
@@ -144,5 +162,3 @@ typeName<double>()
|
||||
}
|
||||
|
||||
}; // namespace clio::detail
|
||||
|
||||
#endif // RIPPLE_APP_CONFIG_DETAIL_H_INCLUDED
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_ETLHELPERS_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_ETLHELPERS_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
@@ -58,14 +77,10 @@ public:
|
||||
/// @return true if sequence was validated, false otherwise
|
||||
/// a return value of false means the datastructure has been stopped
|
||||
bool
|
||||
waitUntilValidatedByNetwork(
|
||||
uint32_t sequence,
|
||||
std::optional<uint32_t> maxWaitMs = {})
|
||||
waitUntilValidatedByNetwork(uint32_t sequence, std::optional<uint32_t> maxWaitMs = {})
|
||||
{
|
||||
std::unique_lock lck(m_);
|
||||
auto pred = [sequence, this]() -> bool {
|
||||
return (max_ && sequence <= *max_);
|
||||
};
|
||||
auto pred = [sequence, this]() -> bool { return (max_ && sequence <= *max_); };
|
||||
if (maxWaitMs)
|
||||
cv_.wait_for(lck, std::chrono::milliseconds(*maxWaitMs));
|
||||
else
|
||||
@@ -140,7 +155,7 @@ public:
|
||||
std::optional<T>
|
||||
tryPop()
|
||||
{
|
||||
std::unique_lock lck(m_);
|
||||
std::scoped_lock lck(m_);
|
||||
if (queue_.empty())
|
||||
return {};
|
||||
T ret = std::move(queue_.front());
|
||||
@@ -171,5 +186,3 @@ getMarkers(size_t numMarkers)
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
#endif // RIPPLE_APP_REPORTING_ETLHELPERS_H_INCLUDED
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,32 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_ETLSOURCE_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_ETLSOURCE_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <config/Config.h>
|
||||
#include <etl/ETLHelpers.h>
|
||||
#include <log/Logger.h>
|
||||
#include <subscriptions/SubscriptionManager.h>
|
||||
|
||||
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
|
||||
#include <grpcpp/grpcpp.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
@@ -7,14 +34,8 @@
|
||||
#include <boost/beast/core/string.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <config/Config.h>
|
||||
#include <etl/ETLHelpers.h>
|
||||
#include <subscriptions/SubscriptionManager.h>
|
||||
|
||||
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
|
||||
#include <grpcpp/grpcpp.h>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
|
||||
class ETLLoadBalancer;
|
||||
class ETLSource;
|
||||
@@ -32,6 +53,7 @@ class ForwardCache
|
||||
{
|
||||
using response_type = std::optional<boost::json::object>;
|
||||
|
||||
clio::Logger log_{"ETL"};
|
||||
mutable std::atomic_bool stopping_ = false;
|
||||
mutable std::shared_mutex mtx_;
|
||||
std::unordered_map<std::string, response_type> latestForwarded_;
|
||||
@@ -45,26 +67,20 @@ class ForwardCache
|
||||
clear();
|
||||
|
||||
public:
|
||||
ForwardCache(
|
||||
clio::Config const& config,
|
||||
boost::asio::io_context& ioc,
|
||||
ETLSource const& source)
|
||||
ForwardCache(clio::Config const& config, boost::asio::io_context& ioc, ETLSource const& source)
|
||||
: strand_(ioc), timer_(strand_), source_(source)
|
||||
{
|
||||
if (config.contains("cache"))
|
||||
{
|
||||
auto commands =
|
||||
config.arrayOrThrow("cache", "ETLSource cache must be array");
|
||||
auto commands = config.arrayOrThrow("cache", "ETLSource cache must be array");
|
||||
|
||||
if (config.contains("cache_duration"))
|
||||
duration_ = config.valueOrThrow<uint32_t>(
|
||||
"cache_duration",
|
||||
"ETLSource cache_duration must be a number");
|
||||
duration_ =
|
||||
config.valueOrThrow<uint32_t>("cache_duration", "ETLSource cache_duration must be a number");
|
||||
|
||||
for (auto const& command : commands)
|
||||
{
|
||||
auto key = command.valueOrThrow<std::string>(
|
||||
"ETLSource forward command must be array of strings");
|
||||
auto key = command.valueOrThrow<std::string>("ETLSource forward command must be array of strings");
|
||||
latestForwarded_[key] = {};
|
||||
}
|
||||
}
|
||||
@@ -106,27 +122,31 @@ public:
|
||||
hasLedger(uint32_t sequence) const = 0;
|
||||
|
||||
virtual std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
|
||||
fetchLedger(
|
||||
uint32_t ledgerSequence,
|
||||
bool getObjects = true,
|
||||
bool getObjectNeighbors = false) = 0;
|
||||
fetchLedger(uint32_t ledgerSequence, bool getObjects = true, bool getObjectNeighbors = false) = 0;
|
||||
|
||||
virtual bool
|
||||
loadInitialLedger(
|
||||
uint32_t sequence,
|
||||
std::uint32_t numMarkers,
|
||||
bool cacheOnly = false) = 0;
|
||||
loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) = 0;
|
||||
|
||||
virtual std::optional<boost::json::object>
|
||||
forwardToRippled(
|
||||
boost::json::object const& request,
|
||||
std::string const& clientIp,
|
||||
boost::asio::yield_context& yield) const = 0;
|
||||
forwardToRippled(boost::json::object const& request, std::string const& clientIp, boost::asio::yield_context& yield)
|
||||
const = 0;
|
||||
|
||||
virtual boost::uuids::uuid
|
||||
token() const = 0;
|
||||
|
||||
virtual ~ETLSource()
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(ETLSource const& other) const
|
||||
{
|
||||
return token() == other.token();
|
||||
}
|
||||
|
||||
protected:
|
||||
clio::Logger log_{"ETL"};
|
||||
|
||||
private:
|
||||
friend ForwardCache;
|
||||
friend ProbingETLSource;
|
||||
@@ -161,7 +181,7 @@ class ETLSourceImpl : public ETLSource
|
||||
|
||||
std::vector<std::pair<uint32_t, uint32_t>> validatedLedgers_;
|
||||
|
||||
std::string validatedLedgersRaw_;
|
||||
std::string validatedLedgersRaw_{"N/A"};
|
||||
|
||||
std::shared_ptr<NetworkValidatedLedgers> networkValidatedLedgers_;
|
||||
|
||||
@@ -186,6 +206,7 @@ class ETLSourceImpl : public ETLSource
|
||||
ETLLoadBalancer& balancer_;
|
||||
|
||||
ForwardCache forwardCache_;
|
||||
boost::uuids::uuid uuid_;
|
||||
|
||||
std::optional<boost::json::object>
|
||||
requestFromRippled(
|
||||
@@ -218,14 +239,12 @@ protected:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __func__ << " : " << toString();
|
||||
log_.trace() << toString();
|
||||
|
||||
auto const host = ip_;
|
||||
auto const port = wsPort_;
|
||||
|
||||
resolver_.async_resolve(host, port, [this](auto ec, auto results) {
|
||||
onResolve(ec, results);
|
||||
});
|
||||
resolver_.async_resolve(host, port, [this](auto ec, auto results) { onResolve(ec, results); });
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -240,6 +259,12 @@ public:
|
||||
return connected_;
|
||||
}
|
||||
|
||||
boost::uuids::uuid
|
||||
token() const override
|
||||
{
|
||||
return uuid_;
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point
|
||||
getLastMsgTime() const
|
||||
{
|
||||
@@ -275,6 +300,9 @@ public:
|
||||
, timer_(ioContext)
|
||||
, hooks_(hooks)
|
||||
{
|
||||
static boost::uuids::random_generator uuidGenerator;
|
||||
uuid_ = uuidGenerator();
|
||||
|
||||
ip_ = config.valueOr<std::string>("ip", {});
|
||||
wsPort_ = config.valueOr<std::string>("ws_port", {});
|
||||
|
||||
@@ -283,23 +311,18 @@ public:
|
||||
grpcPort_ = *value;
|
||||
try
|
||||
{
|
||||
boost::asio::ip::tcp::endpoint endpoint{
|
||||
boost::asio::ip::make_address(ip_), std::stoi(grpcPort_)};
|
||||
boost::asio::ip::tcp::endpoint endpoint{boost::asio::ip::make_address(ip_), std::stoi(grpcPort_)};
|
||||
std::stringstream ss;
|
||||
ss << endpoint;
|
||||
grpc::ChannelArguments chArgs;
|
||||
chArgs.SetMaxReceiveMessageSize(-1);
|
||||
stub_ = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateCustomChannel(
|
||||
ss.str(), grpc::InsecureChannelCredentials(), chArgs));
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "Made stub for remote = " << toString();
|
||||
grpc::CreateCustomChannel(ss.str(), grpc::InsecureChannelCredentials(), chArgs));
|
||||
log_.debug() << "Made stub for remote = " << toString();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "Exception while creating stub = " << e.what()
|
||||
<< " . Remote = " << toString();
|
||||
log_.debug() << "Exception while creating stub = " << e.what() << " . Remote = " << toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,9 +378,7 @@ public:
|
||||
pairs.push_back(std::make_pair(min, max));
|
||||
}
|
||||
}
|
||||
std::sort(pairs.begin(), pairs.end(), [](auto left, auto right) {
|
||||
return left.first < right.first;
|
||||
});
|
||||
std::sort(pairs.begin(), pairs.end(), [](auto left, auto right) { return left.first < right.first; });
|
||||
|
||||
// we only hold the lock here, to avoid blocking while string processing
|
||||
std::lock_guard lck(mtx_);
|
||||
@@ -371,7 +392,6 @@ public:
|
||||
getValidatedRange() const
|
||||
{
|
||||
std::lock_guard lck(mtx_);
|
||||
|
||||
return validatedLedgersRaw_;
|
||||
}
|
||||
|
||||
@@ -381,17 +401,13 @@ public:
|
||||
/// and the prior one
|
||||
/// @return the extracted data and the result status
|
||||
std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
|
||||
fetchLedger(
|
||||
uint32_t ledgerSequence,
|
||||
bool getObjects = true,
|
||||
bool getObjectNeighbors = false) override;
|
||||
fetchLedger(uint32_t ledgerSequence, bool getObjects = true, bool getObjectNeighbors = false) override;
|
||||
|
||||
std::string
|
||||
toString() const override
|
||||
{
|
||||
return "{ validated_ledger : " + getValidatedRange() +
|
||||
" , ip : " + ip_ + " , web socket port : " + wsPort_ +
|
||||
", grpc port : " + grpcPort_ + " }";
|
||||
return "{validated_ledger: " + getValidatedRange() + ", ip: " + ip_ + ", web socket port: " + wsPort_ +
|
||||
", grpc port: " + grpcPort_ + "}";
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
@@ -406,8 +422,7 @@ public:
|
||||
auto last = getLastMsgTime();
|
||||
if (last.time_since_epoch().count() != 0)
|
||||
res["last_msg_age_seconds"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now() - getLastMsgTime())
|
||||
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - getLastMsgTime())
|
||||
.count());
|
||||
return res;
|
||||
}
|
||||
@@ -417,10 +432,7 @@ public:
|
||||
/// @param writeQueue queue to push downloaded ledger objects
|
||||
/// @return true if the download was successful
|
||||
bool
|
||||
loadInitialLedger(
|
||||
std::uint32_t ledgerSequence,
|
||||
std::uint32_t numMarkers,
|
||||
bool cacheOnly = false) override;
|
||||
loadInitialLedger(std::uint32_t ledgerSequence, std::uint32_t numMarkers, bool cacheOnly = false) override;
|
||||
|
||||
/// Attempt to reconnect to the ETL source
|
||||
void
|
||||
@@ -444,16 +456,11 @@ public:
|
||||
|
||||
/// Callback
|
||||
void
|
||||
onResolve(
|
||||
boost::beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type results);
|
||||
onResolve(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results);
|
||||
|
||||
/// Callback
|
||||
virtual void
|
||||
onConnect(
|
||||
boost::beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type::endpoint_type
|
||||
endpoint) = 0;
|
||||
onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint) = 0;
|
||||
|
||||
/// Callback
|
||||
void
|
||||
@@ -473,16 +480,13 @@ public:
|
||||
handleMessage();
|
||||
|
||||
std::optional<boost::json::object>
|
||||
forwardToRippled(
|
||||
boost::json::object const& request,
|
||||
std::string const& clientIp,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
forwardToRippled(boost::json::object const& request, std::string const& clientIp, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
};
|
||||
|
||||
class PlainETLSource : public ETLSourceImpl<PlainETLSource>
|
||||
{
|
||||
std::unique_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>>
|
||||
ws_;
|
||||
std::unique_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws_;
|
||||
|
||||
public:
|
||||
PlainETLSource(
|
||||
@@ -493,24 +497,14 @@ public:
|
||||
std::shared_ptr<NetworkValidatedLedgers> nwvl,
|
||||
ETLLoadBalancer& balancer,
|
||||
ETLSourceHooks hooks)
|
||||
: ETLSourceImpl(
|
||||
config,
|
||||
ioc,
|
||||
backend,
|
||||
subscriptions,
|
||||
nwvl,
|
||||
balancer,
|
||||
std::move(hooks))
|
||||
, ws_(std::make_unique<
|
||||
boost::beast::websocket::stream<boost::beast::tcp_stream>>(
|
||||
: ETLSourceImpl(config, ioc, backend, subscriptions, nwvl, balancer, std::move(hooks))
|
||||
, ws_(std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(
|
||||
boost::asio::make_strand(ioc)))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
onConnect(
|
||||
boost::beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint)
|
||||
onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint)
|
||||
override;
|
||||
|
||||
/// Close the websocket
|
||||
@@ -529,9 +523,7 @@ class SslETLSource : public ETLSourceImpl<SslETLSource>
|
||||
{
|
||||
std::optional<std::reference_wrapper<boost::asio::ssl::context>> sslCtx_;
|
||||
|
||||
std::unique_ptr<boost::beast::websocket::stream<
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>>>
|
||||
ws_;
|
||||
std::unique_ptr<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>> ws_;
|
||||
|
||||
public:
|
||||
SslETLSource(
|
||||
@@ -543,40 +535,27 @@ public:
|
||||
std::shared_ptr<NetworkValidatedLedgers> nwvl,
|
||||
ETLLoadBalancer& balancer,
|
||||
ETLSourceHooks hooks)
|
||||
: ETLSourceImpl(
|
||||
config,
|
||||
ioc,
|
||||
backend,
|
||||
subscriptions,
|
||||
nwvl,
|
||||
balancer,
|
||||
std::move(hooks))
|
||||
: ETLSourceImpl(config, ioc, backend, subscriptions, nwvl, balancer, std::move(hooks))
|
||||
, sslCtx_(sslCtx)
|
||||
, ws_(std::make_unique<boost::beast::websocket::stream<
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>>>(
|
||||
, ws_(std::make_unique<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>>(
|
||||
boost::asio::make_strand(ioc_),
|
||||
*sslCtx_))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
onConnect(
|
||||
boost::beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint)
|
||||
onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint)
|
||||
override;
|
||||
|
||||
void
|
||||
onSslHandshake(
|
||||
boost::beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint);
|
||||
onSslHandshake(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint);
|
||||
|
||||
/// Close the websocket
|
||||
/// @param startAgain whether to reconnect
|
||||
void
|
||||
close(bool startAgain);
|
||||
|
||||
boost::beast::websocket::stream<
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>>&
|
||||
boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>&
|
||||
ws()
|
||||
{
|
||||
return *ws_;
|
||||
@@ -592,8 +571,8 @@ public:
|
||||
class ETLLoadBalancer
|
||||
{
|
||||
private:
|
||||
clio::Logger log_{"ETL"};
|
||||
std::vector<std::unique_ptr<ETLSource>> sources_;
|
||||
|
||||
std::uint32_t downloadRanges_ = 16;
|
||||
|
||||
public:
|
||||
@@ -612,8 +591,7 @@ public:
|
||||
std::shared_ptr<SubscriptionManager> subscriptions,
|
||||
std::shared_ptr<NetworkValidatedLedgers> validatedLedgers)
|
||||
{
|
||||
return std::make_shared<ETLLoadBalancer>(
|
||||
config, ioc, backend, subscriptions, validatedLedgers);
|
||||
return std::make_shared<ETLLoadBalancer>(config, ioc, backend, subscriptions, validatedLedgers);
|
||||
}
|
||||
|
||||
~ETLLoadBalancer()
|
||||
@@ -636,10 +614,7 @@ public:
|
||||
/// was found in the database or the server is shutting down, the optional
|
||||
/// will be empty
|
||||
std::optional<org::xrpl::rpc::v1::GetLedgerResponse>
|
||||
fetchLedger(
|
||||
uint32_t ledgerSequence,
|
||||
bool getObjects,
|
||||
bool getObjectNeighbors);
|
||||
fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObjectNeighbors);
|
||||
|
||||
/// Determine whether messages received on the transactions_proposed stream
|
||||
/// should be forwarded to subscribing clients. The server subscribes to
|
||||
@@ -657,10 +632,7 @@ public:
|
||||
// We pick the first ETLSource encountered that is connected
|
||||
if (src->isConnected())
|
||||
{
|
||||
if (src.get() == in)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
return *src == *in;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,10 +655,8 @@ public:
|
||||
/// @param request JSON-RPC request
|
||||
/// @return response received from rippled node
|
||||
std::optional<boost::json::object>
|
||||
forwardToRippled(
|
||||
boost::json::object const& request,
|
||||
std::string const& clientIp,
|
||||
boost::asio::yield_context& yield) const;
|
||||
forwardToRippled(boost::json::object const& request, std::string const& clientIp, boost::asio::yield_context& yield)
|
||||
const;
|
||||
|
||||
private:
|
||||
/// f is a function that takes an ETLSource as an argument and returns a
|
||||
@@ -703,5 +673,3 @@ private:
|
||||
bool
|
||||
execute(Func f, uint32_t ledgerSequence);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
@@ -26,25 +44,18 @@ getNFTokenMintData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
|
||||
for (ripple::STObject const& node : txMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) !=
|
||||
ripple::ltNFTOKEN_PAGE)
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_PAGE)
|
||||
continue;
|
||||
|
||||
if (!owner)
|
||||
owner = ripple::AccountID::fromVoid(
|
||||
node.getFieldH256(ripple::sfLedgerIndex).data());
|
||||
owner = ripple::AccountID::fromVoid(node.getFieldH256(ripple::sfLedgerIndex).data());
|
||||
|
||||
if (node.getFName() == ripple::sfCreatedNode)
|
||||
{
|
||||
ripple::STArray const& toAddNFTs =
|
||||
node.peekAtField(ripple::sfNewFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldArray(ripple::sfNFTokens);
|
||||
node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>().getFieldArray(ripple::sfNFTokens);
|
||||
std::transform(
|
||||
toAddNFTs.begin(),
|
||||
toAddNFTs.end(),
|
||||
std::back_inserter(finalIDs),
|
||||
[](ripple::STObject const& nft) {
|
||||
toAddNFTs.begin(), toAddNFTs.end(), std::back_inserter(finalIDs), [](ripple::STObject const& nft) {
|
||||
return nft.getFieldH256(ripple::sfNFTokenID);
|
||||
});
|
||||
}
|
||||
@@ -62,32 +73,23 @@ getNFTokenMintData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
// as rippled outputs all fields in final fields even if they were
|
||||
// not changed.
|
||||
ripple::STObject const& previousFields =
|
||||
node.peekAtField(ripple::sfPreviousFields)
|
||||
.downcast<ripple::STObject>();
|
||||
node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
|
||||
if (!previousFields.isFieldPresent(ripple::sfNFTokens))
|
||||
continue;
|
||||
|
||||
ripple::STArray const& toAddNFTs =
|
||||
previousFields.getFieldArray(ripple::sfNFTokens);
|
||||
ripple::STArray const& toAddNFTs = previousFields.getFieldArray(ripple::sfNFTokens);
|
||||
std::transform(
|
||||
toAddNFTs.begin(),
|
||||
toAddNFTs.end(),
|
||||
std::back_inserter(prevIDs),
|
||||
[](ripple::STObject const& nft) {
|
||||
toAddNFTs.begin(), toAddNFTs.end(), std::back_inserter(prevIDs), [](ripple::STObject const& nft) {
|
||||
return nft.getFieldH256(ripple::sfNFTokenID);
|
||||
});
|
||||
|
||||
ripple::STArray const& toAddFinalNFTs =
|
||||
node.peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldArray(ripple::sfNFTokens);
|
||||
node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>().getFieldArray(ripple::sfNFTokens);
|
||||
std::transform(
|
||||
toAddFinalNFTs.begin(),
|
||||
toAddFinalNFTs.end(),
|
||||
std::back_inserter(finalIDs),
|
||||
[](ripple::STObject const& nft) {
|
||||
return nft.getFieldH256(ripple::sfNFTokenID);
|
||||
});
|
||||
[](ripple::STObject const& nft) { return nft.getFieldH256(ripple::sfNFTokenID); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,13 +104,11 @@ getNFTokenMintData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
std::inserter(tokenIDResult, tokenIDResult.begin()));
|
||||
if (tokenIDResult.size() == 1 && owner)
|
||||
return {
|
||||
{NFTTransactionsData(
|
||||
tokenIDResult.front(), txMeta, sttx.getTransactionID())},
|
||||
NFTsData(tokenIDResult.front(), *owner, txMeta, false)};
|
||||
{NFTTransactionsData(tokenIDResult.front(), txMeta, sttx.getTransactionID())},
|
||||
NFTsData(tokenIDResult.front(), *owner, sttx.getFieldVL(ripple::sfURI), txMeta)};
|
||||
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - unexpected NFTokenMint data in tx "
|
||||
<< sttx.getTransactionID();
|
||||
msg << " - unexpected NFTokenMint data in tx " << sttx.getTransactionID();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
|
||||
@@ -116,49 +116,43 @@ std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTokenBurnData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
ripple::uint256 const tokenID = sttx.getFieldH256(ripple::sfNFTokenID);
|
||||
std::vector<NFTTransactionsData> const txs = {
|
||||
NFTTransactionsData(tokenID, txMeta, sttx.getTransactionID())};
|
||||
std::vector<NFTTransactionsData> const txs = {NFTTransactionsData(tokenID, txMeta, sttx.getTransactionID())};
|
||||
|
||||
// Determine who owned the token when it was burned by finding an
|
||||
// NFTokenPage that was deleted or modified that contains this
|
||||
// tokenID.
|
||||
for (ripple::STObject const& node : txMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) !=
|
||||
ripple::ltNFTOKEN_PAGE ||
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_PAGE ||
|
||||
node.getFName() == ripple::sfCreatedNode)
|
||||
continue;
|
||||
|
||||
// NFT burn can result in an NFTokenPage being modified to no longer
|
||||
// include the target, or an NFTokenPage being deleted. If this is
|
||||
// modified, we want to look for the target in the fields prior to
|
||||
// modification. If deleted, it's possible that the page was modified
|
||||
// to remove the target NFT prior to the entire page being deleted. In
|
||||
// this case, we need to look in the PreviousFields. Otherwise, the
|
||||
// page was not modified prior to deleting and we need to look in the
|
||||
// FinalFields.
|
||||
// modification. If deleted, it's possible that the page was
|
||||
// modified to remove the target NFT prior to the entire page being
|
||||
// deleted. In this case, we need to look in the PreviousFields.
|
||||
// Otherwise, the page was not modified prior to deleting and we
|
||||
// need to look in the FinalFields.
|
||||
std::optional<ripple::STArray> prevNFTs;
|
||||
|
||||
if (node.isFieldPresent(ripple::sfPreviousFields))
|
||||
{
|
||||
ripple::STObject const& previousFields =
|
||||
node.peekAtField(ripple::sfPreviousFields)
|
||||
.downcast<ripple::STObject>();
|
||||
node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
|
||||
if (previousFields.isFieldPresent(ripple::sfNFTokens))
|
||||
prevNFTs = previousFields.getFieldArray(ripple::sfNFTokens);
|
||||
}
|
||||
else if (!prevNFTs && node.getFName() == ripple::sfDeletedNode)
|
||||
prevNFTs = node.peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldArray(ripple::sfNFTokens);
|
||||
prevNFTs =
|
||||
node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>().getFieldArray(ripple::sfNFTokens);
|
||||
|
||||
if (!prevNFTs)
|
||||
continue;
|
||||
|
||||
auto const nft = std::find_if(
|
||||
prevNFTs->begin(),
|
||||
prevNFTs->end(),
|
||||
[&tokenID](ripple::STObject const& candidate) {
|
||||
auto const nft =
|
||||
std::find_if(prevNFTs->begin(), prevNFTs->end(), [&tokenID](ripple::STObject const& candidate) {
|
||||
return candidate.getFieldH256(ripple::sfNFTokenID) == tokenID;
|
||||
});
|
||||
if (nft != prevNFTs->end())
|
||||
@@ -166,92 +160,74 @@ getNFTokenBurnData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
txs,
|
||||
NFTsData(
|
||||
tokenID,
|
||||
ripple::AccountID::fromVoid(
|
||||
node.getFieldH256(ripple::sfLedgerIndex).data()),
|
||||
ripple::AccountID::fromVoid(node.getFieldH256(ripple::sfLedgerIndex).data()),
|
||||
txMeta,
|
||||
true));
|
||||
}
|
||||
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - could not determine owner at burntime for tx "
|
||||
<< sttx.getTransactionID();
|
||||
msg << " - could not determine owner at burntime for tx " << sttx.getTransactionID();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTokenAcceptOfferData(
|
||||
ripple::TxMeta const& txMeta,
|
||||
ripple::STTx const& sttx)
|
||||
getNFTokenAcceptOfferData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
// If we have the buy offer from this tx, we can determine the owner
|
||||
// more easily by just looking at the owner of the accepted NFTokenOffer
|
||||
// object.
|
||||
if (sttx.isFieldPresent(ripple::sfNFTokenBuyOffer))
|
||||
{
|
||||
auto const affectedBuyOffer = std::find_if(
|
||||
txMeta.getNodes().begin(),
|
||||
txMeta.getNodes().end(),
|
||||
[&sttx](ripple::STObject const& node) {
|
||||
return node.getFieldH256(ripple::sfLedgerIndex) ==
|
||||
sttx.getFieldH256(ripple::sfNFTokenBuyOffer);
|
||||
auto const affectedBuyOffer =
|
||||
std::find_if(txMeta.getNodes().begin(), txMeta.getNodes().end(), [&sttx](ripple::STObject const& node) {
|
||||
return node.getFieldH256(ripple::sfLedgerIndex) == sttx.getFieldH256(ripple::sfNFTokenBuyOffer);
|
||||
});
|
||||
if (affectedBuyOffer == txMeta.getNodes().end())
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - unexpected NFTokenAcceptOffer data in tx "
|
||||
<< sttx.getTransactionID();
|
||||
msg << " - unexpected NFTokenAcceptOffer data in tx " << sttx.getTransactionID();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
|
||||
ripple::uint256 const tokenID =
|
||||
affectedBuyOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldH256(ripple::sfNFTokenID);
|
||||
ripple::uint256 const tokenID = affectedBuyOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldH256(ripple::sfNFTokenID);
|
||||
|
||||
ripple::AccountID const owner =
|
||||
affectedBuyOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getAccountID(ripple::sfOwner);
|
||||
ripple::AccountID const owner = affectedBuyOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getAccountID(ripple::sfOwner);
|
||||
return {
|
||||
{NFTTransactionsData(tokenID, txMeta, sttx.getTransactionID())},
|
||||
NFTsData(tokenID, owner, txMeta, false)};
|
||||
{NFTTransactionsData(tokenID, txMeta, sttx.getTransactionID())}, NFTsData(tokenID, owner, txMeta, false)};
|
||||
}
|
||||
|
||||
// Otherwise we have to infer the new owner from the affected nodes.
|
||||
auto const affectedSellOffer = std::find_if(
|
||||
txMeta.getNodes().begin(),
|
||||
txMeta.getNodes().end(),
|
||||
[&sttx](ripple::STObject const& node) {
|
||||
return node.getFieldH256(ripple::sfLedgerIndex) ==
|
||||
sttx.getFieldH256(ripple::sfNFTokenSellOffer);
|
||||
auto const affectedSellOffer =
|
||||
std::find_if(txMeta.getNodes().begin(), txMeta.getNodes().end(), [&sttx](ripple::STObject const& node) {
|
||||
return node.getFieldH256(ripple::sfLedgerIndex) == sttx.getFieldH256(ripple::sfNFTokenSellOffer);
|
||||
});
|
||||
if (affectedSellOffer == txMeta.getNodes().end())
|
||||
{
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - unexpected NFTokenAcceptOffer data in tx "
|
||||
<< sttx.getTransactionID();
|
||||
msg << " - unexpected NFTokenAcceptOffer data in tx " << sttx.getTransactionID();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
|
||||
ripple::uint256 const tokenID =
|
||||
affectedSellOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldH256(ripple::sfNFTokenID);
|
||||
ripple::uint256 const tokenID = affectedSellOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldH256(ripple::sfNFTokenID);
|
||||
|
||||
ripple::AccountID const seller =
|
||||
affectedSellOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getAccountID(ripple::sfOwner);
|
||||
ripple::AccountID const seller = affectedSellOffer->peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getAccountID(ripple::sfOwner);
|
||||
|
||||
for (ripple::STObject const& node : txMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) !=
|
||||
ripple::ltNFTOKEN_PAGE ||
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_PAGE ||
|
||||
node.getFName() == ripple::sfDeletedNode)
|
||||
continue;
|
||||
|
||||
ripple::AccountID const nodeOwner = ripple::AccountID::fromVoid(
|
||||
node.getFieldH256(ripple::sfLedgerIndex).data());
|
||||
ripple::AccountID const nodeOwner =
|
||||
ripple::AccountID::fromVoid(node.getFieldH256(ripple::sfLedgerIndex).data());
|
||||
if (nodeOwner == seller)
|
||||
continue;
|
||||
|
||||
@@ -265,12 +241,9 @@ getNFTokenAcceptOfferData(
|
||||
.getFieldArray(ripple::sfNFTokens);
|
||||
}();
|
||||
|
||||
auto const nft = std::find_if(
|
||||
nfts.begin(),
|
||||
nfts.end(),
|
||||
[&tokenID](ripple::STObject const& candidate) {
|
||||
return candidate.getFieldH256(ripple::sfNFTokenID) == tokenID;
|
||||
});
|
||||
auto const nft = std::find_if(nfts.begin(), nfts.end(), [&tokenID](ripple::STObject const& candidate) {
|
||||
return candidate.getFieldH256(ripple::sfNFTokenID) == tokenID;
|
||||
});
|
||||
if (nft != nfts.end())
|
||||
return {
|
||||
{NFTTransactionsData(tokenID, txMeta, sttx.getTransactionID())},
|
||||
@@ -278,8 +251,7 @@ getNFTokenAcceptOfferData(
|
||||
}
|
||||
|
||||
std::stringstream msg;
|
||||
msg << __func__ << " - unexpected NFTokenAcceptOffer data in tx "
|
||||
<< sttx.getTransactionID();
|
||||
msg << " - unexpected NFTokenAcceptOffer data in tx " << sttx.getTransactionID();
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
|
||||
@@ -288,40 +260,28 @@ getNFTokenAcceptOfferData(
|
||||
// transaction using this feature. This transaction also never returns an
|
||||
// NFTsData because it does not change the state of an NFT itself.
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTokenCancelOfferData(
|
||||
ripple::TxMeta const& txMeta,
|
||||
ripple::STTx const& sttx)
|
||||
getNFTokenCancelOfferData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
std::vector<NFTTransactionsData> txs;
|
||||
for (ripple::STObject const& node : txMeta.getNodes())
|
||||
{
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) !=
|
||||
ripple::ltNFTOKEN_OFFER)
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_OFFER)
|
||||
continue;
|
||||
|
||||
ripple::uint256 const tokenID = node.peekAtField(ripple::sfFinalFields)
|
||||
.downcast<ripple::STObject>()
|
||||
.getFieldH256(ripple::sfNFTokenID);
|
||||
ripple::uint256 const tokenID =
|
||||
node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>().getFieldH256(ripple::sfNFTokenID);
|
||||
txs.emplace_back(tokenID, txMeta, sttx.getTransactionID());
|
||||
}
|
||||
|
||||
// Deduplicate any transactions based on tokenID/txIdx combo. Can't just
|
||||
// use txIdx because in this case one tx can cancel offers for several
|
||||
// NFTs.
|
||||
std::sort(
|
||||
txs.begin(),
|
||||
txs.end(),
|
||||
[](NFTTransactionsData const& a, NFTTransactionsData const& b) {
|
||||
return a.tokenID < b.tokenID &&
|
||||
a.transactionIndex < b.transactionIndex;
|
||||
});
|
||||
auto last = std::unique(
|
||||
txs.begin(),
|
||||
txs.end(),
|
||||
[](NFTTransactionsData const& a, NFTTransactionsData const& b) {
|
||||
return a.tokenID == b.tokenID &&
|
||||
a.transactionIndex == b.transactionIndex;
|
||||
});
|
||||
std::sort(txs.begin(), txs.end(), [](NFTTransactionsData const& a, NFTTransactionsData const& b) {
|
||||
return a.tokenID < b.tokenID && a.transactionIndex < b.transactionIndex;
|
||||
});
|
||||
auto last = std::unique(txs.begin(), txs.end(), [](NFTTransactionsData const& a, NFTTransactionsData const& b) {
|
||||
return a.tokenID == b.tokenID && a.transactionIndex == b.transactionIndex;
|
||||
});
|
||||
txs.erase(last, txs.end());
|
||||
return {txs, {}};
|
||||
}
|
||||
@@ -329,20 +289,13 @@ getNFTokenCancelOfferData(
|
||||
// This transaction never returns an NFTokensData because it does not
|
||||
// change the state of an NFT itself.
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTokenCreateOfferData(
|
||||
ripple::TxMeta const& txMeta,
|
||||
ripple::STTx const& sttx)
|
||||
getNFTokenCreateOfferData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
return {
|
||||
{NFTTransactionsData(
|
||||
sttx.getFieldH256(ripple::sfNFTokenID),
|
||||
txMeta,
|
||||
sttx.getTransactionID())},
|
||||
{}};
|
||||
return {{NFTTransactionsData(sttx.getFieldH256(ripple::sfNFTokenID), txMeta, sttx.getTransactionID())}, {}};
|
||||
}
|
||||
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
getNFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
if (txMeta.getResultTER() != ripple::tesSUCCESS)
|
||||
return {{}, {}};
|
||||
@@ -368,3 +321,20 @@ getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
return {{}, {}};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<NFTsData>
|
||||
getNFTDataFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob)
|
||||
{
|
||||
std::vector<NFTsData> nfts;
|
||||
ripple::STLedgerEntry const sle =
|
||||
ripple::STLedgerEntry(ripple::SerialIter{blob.data(), blob.size()}, ripple::uint256::fromVoid(key.data()));
|
||||
|
||||
if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltNFTOKEN_PAGE)
|
||||
return nfts;
|
||||
|
||||
auto const owner = ripple::AccountID::fromVoid(key.data());
|
||||
for (ripple::STObject const& node : sle.getFieldArray(ripple::sfNFTokens))
|
||||
nfts.emplace_back(node.getFieldH256(ripple::sfNFTokenID), seq, owner, node.getFieldVL(ripple::sfURI));
|
||||
|
||||
return nfts;
|
||||
}
|
||||
|
||||
33
src/etl/NFTHelpers.h
Normal file
33
src/etl/NFTHelpers.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/DBHelpers.h>
|
||||
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
|
||||
// Pulling from tx via ReportingETL
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
|
||||
|
||||
// Pulling from ledger object via loadInitialLedger
|
||||
std::vector<NFTsData>
|
||||
getNFTDataFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob);
|
||||
@@ -1,4 +1,26 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <etl/ProbingETLSource.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
using namespace clio;
|
||||
|
||||
ProbingETLSource::ProbingETLSource(
|
||||
clio::Config const& config,
|
||||
@@ -9,23 +31,9 @@ ProbingETLSource::ProbingETLSource(
|
||||
ETLLoadBalancer& balancer,
|
||||
boost::asio::ssl::context sslCtx)
|
||||
: sslCtx_{std::move(sslCtx)}
|
||||
, sslSrc_{make_shared<SslETLSource>(
|
||||
config,
|
||||
ioc,
|
||||
std::ref(sslCtx_),
|
||||
backend,
|
||||
subscriptions,
|
||||
nwvl,
|
||||
balancer,
|
||||
make_SSLHooks())}
|
||||
, plainSrc_{make_shared<PlainETLSource>(
|
||||
config,
|
||||
ioc,
|
||||
backend,
|
||||
subscriptions,
|
||||
nwvl,
|
||||
balancer,
|
||||
make_PlainHooks())}
|
||||
, sslSrc_{make_shared<
|
||||
SslETLSource>(config, ioc, std::ref(sslCtx_), backend, subscriptions, nwvl, balancer, make_SSLHooks())}
|
||||
, plainSrc_{make_shared<PlainETLSource>(config, ioc, backend, subscriptions, nwvl, balancer, make_PlainHooks())}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,32 +93,32 @@ std::string
|
||||
ProbingETLSource::toString() const
|
||||
{
|
||||
if (!currentSrc_)
|
||||
return "{ probing }";
|
||||
return "{probing... ws: " + plainSrc_->toString() + ", wss: " + sslSrc_->toString() + "}";
|
||||
return currentSrc_->toString();
|
||||
}
|
||||
|
||||
boost::uuids::uuid
|
||||
ProbingETLSource::token() const
|
||||
{
|
||||
if (!currentSrc_)
|
||||
return boost::uuids::nil_uuid();
|
||||
return currentSrc_->token();
|
||||
}
|
||||
|
||||
bool
|
||||
ProbingETLSource::loadInitialLedger(
|
||||
std::uint32_t ledgerSequence,
|
||||
std::uint32_t numMarkers,
|
||||
bool cacheOnly)
|
||||
ProbingETLSource::loadInitialLedger(std::uint32_t ledgerSequence, std::uint32_t numMarkers, bool cacheOnly)
|
||||
{
|
||||
if (!currentSrc_)
|
||||
return false;
|
||||
return currentSrc_->loadInitialLedger(
|
||||
ledgerSequence, numMarkers, cacheOnly);
|
||||
return currentSrc_->loadInitialLedger(ledgerSequence, numMarkers, cacheOnly);
|
||||
}
|
||||
|
||||
std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
|
||||
ProbingETLSource::fetchLedger(
|
||||
uint32_t ledgerSequence,
|
||||
bool getObjects,
|
||||
bool getObjectNeighbors)
|
||||
ProbingETLSource::fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObjectNeighbors)
|
||||
{
|
||||
if (!currentSrc_)
|
||||
return {};
|
||||
return currentSrc_->fetchLedger(
|
||||
ledgerSequence, getObjects, getObjectNeighbors);
|
||||
return currentSrc_->fetchLedger(ledgerSequence, getObjects, getObjectNeighbors);
|
||||
}
|
||||
|
||||
std::optional<boost::json::object>
|
||||
@@ -148,9 +156,7 @@ ProbingETLSource::make_SSLHooks() noexcept
|
||||
{
|
||||
plainSrc_->pause();
|
||||
currentSrc_ = sslSrc_;
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "Selected WSS as the main source: "
|
||||
<< currentSrc_->toString();
|
||||
log_.info() << "Selected WSS as the main source: " << currentSrc_->toString();
|
||||
}
|
||||
return ETLSourceHooks::Action::PROCEED;
|
||||
},
|
||||
@@ -179,9 +185,7 @@ ProbingETLSource::make_PlainHooks() noexcept
|
||||
{
|
||||
sslSrc_->pause();
|
||||
currentSrc_ = plainSrc_;
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "Selected Plain WS as the main source: "
|
||||
<< currentSrc_->toString();
|
||||
log_.info() << "Selected Plain WS as the main source: " << currentSrc_->toString();
|
||||
}
|
||||
return ETLSourceHooks::Action::PROCEED;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_PROBINGETLSOURCE_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_PROBINGETLSOURCE_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
@@ -11,6 +29,7 @@
|
||||
|
||||
#include <config/Config.h>
|
||||
#include <etl/ETLSource.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
/// This ETLSource implementation attempts to connect over both secure websocket
|
||||
/// and plain websocket. First to connect pauses the other and the probing is
|
||||
@@ -18,6 +37,8 @@
|
||||
/// connection the probing is kickstarted again.
|
||||
class ProbingETLSource : public ETLSource
|
||||
{
|
||||
clio::Logger log_{"ETL"};
|
||||
|
||||
std::mutex mtx_;
|
||||
boost::asio::ssl::context sslCtx_;
|
||||
std::shared_ptr<ETLSource> sslSrc_;
|
||||
@@ -32,8 +53,7 @@ public:
|
||||
std::shared_ptr<SubscriptionManager> subscriptions,
|
||||
std::shared_ptr<NetworkValidatedLedgers> nwvl,
|
||||
ETLLoadBalancer& balancer,
|
||||
boost::asio::ssl::context sslCtx = boost::asio::ssl::context{
|
||||
boost::asio::ssl::context::tlsv12});
|
||||
boost::asio::ssl::context sslCtx = boost::asio::ssl::context{boost::asio::ssl::context::tlsv12});
|
||||
|
||||
~ProbingETLSource() = default;
|
||||
|
||||
@@ -59,22 +79,17 @@ public:
|
||||
toString() const override;
|
||||
|
||||
bool
|
||||
loadInitialLedger(
|
||||
std::uint32_t ledgerSequence,
|
||||
std::uint32_t numMarkers,
|
||||
bool cacheOnly = false) override;
|
||||
loadInitialLedger(std::uint32_t ledgerSequence, std::uint32_t numMarkers, bool cacheOnly = false) override;
|
||||
|
||||
std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
|
||||
fetchLedger(
|
||||
uint32_t ledgerSequence,
|
||||
bool getObjects = true,
|
||||
bool getObjectNeighbors = false) override;
|
||||
fetchLedger(uint32_t ledgerSequence, bool getObjects = true, bool getObjectNeighbors = false) override;
|
||||
|
||||
std::optional<boost::json::object>
|
||||
forwardToRippled(
|
||||
boost::json::object const& request,
|
||||
std::string const& clientIp,
|
||||
boost::asio::yield_context& yield) const override;
|
||||
forwardToRippled(boost::json::object const& request, std::string const& clientIp, boost::asio::yield_context& yield)
|
||||
const override;
|
||||
|
||||
boost::uuids::uuid
|
||||
token() const override;
|
||||
|
||||
private:
|
||||
std::optional<boost::json::object>
|
||||
@@ -89,5 +104,3 @@ private:
|
||||
ETLSourceHooks
|
||||
make_PlainHooks() noexcept;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,23 @@
|
||||
#ifndef RIPPLE_APP_REPORTING_REPORTINGETL_H_INCLUDED
|
||||
#define RIPPLE_APP_REPORTING_REPORTINGETL_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -8,6 +26,7 @@
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <etl/ETLSource.h>
|
||||
#include <log/Logger.h>
|
||||
#include <subscriptions/SubscriptionManager.h>
|
||||
|
||||
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
|
||||
@@ -19,13 +38,6 @@
|
||||
|
||||
#include <chrono>
|
||||
|
||||
/**
|
||||
* Helper function for the ReportingETL, implemented in NFTHelpers.cpp, to
|
||||
* pull to-write data out of a transaction that relates to NFTs.
|
||||
*/
|
||||
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
|
||||
getNFTData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
|
||||
|
||||
struct AccountTransactionsData;
|
||||
struct NFTTransactionsData;
|
||||
struct NFTsData;
|
||||
@@ -55,6 +67,8 @@ class SubscriptionManager;
|
||||
class ReportingETL
|
||||
{
|
||||
private:
|
||||
clio::Logger log_{"ETL"};
|
||||
|
||||
std::shared_ptr<BackendInterface> backend_;
|
||||
std::shared_ptr<SubscriptionManager> subscriptions_;
|
||||
std::shared_ptr<ETLLoadBalancer> loadBalancer_;
|
||||
@@ -153,7 +167,7 @@ private:
|
||||
void
|
||||
setLastPublish()
|
||||
{
|
||||
std::unique_lock lck(publishTimeMtx_);
|
||||
std::scoped_lock lck(publishTimeMtx_);
|
||||
lastPublish_ = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
@@ -165,7 +179,7 @@ private:
|
||||
void
|
||||
setLastClose(std::chrono::time_point<ripple::NetClock> lastCloseTime)
|
||||
{
|
||||
std::unique_lock lck(closeTimeMtx_);
|
||||
std::scoped_lock lck(closeTimeMtx_);
|
||||
lastCloseTime_ = lastCloseTime;
|
||||
}
|
||||
|
||||
@@ -251,9 +265,7 @@ private:
|
||||
/// (mostly transaction hashes, corresponding nodestore hashes and affected
|
||||
/// accounts)
|
||||
FormattedTransactionsData
|
||||
insertTransactions(
|
||||
ripple::LedgerInfo const& ledger,
|
||||
org::xrpl::rpc::v1::GetLedgerResponse& data);
|
||||
insertTransactions(ripple::LedgerInfo const& ledger, org::xrpl::rpc::v1::GetLedgerResponse& data);
|
||||
|
||||
// TODO update this documentation
|
||||
/// Build the next ledger using the previous ledger and the extracted data.
|
||||
@@ -300,7 +312,7 @@ private:
|
||||
void
|
||||
run()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Starting reporting etl";
|
||||
log_.info() << "Starting reporting etl";
|
||||
stopping_ = false;
|
||||
|
||||
doWork();
|
||||
@@ -327,8 +339,7 @@ public:
|
||||
std::shared_ptr<ETLLoadBalancer> balancer,
|
||||
std::shared_ptr<NetworkValidatedLedgers> ledgers)
|
||||
{
|
||||
auto etl = std::make_shared<ReportingETL>(
|
||||
config, ioc, backend, subscriptions, balancer, ledgers);
|
||||
auto etl = std::make_shared<ReportingETL>(config, ioc, backend, subscriptions, balancer, ledgers);
|
||||
|
||||
etl->run();
|
||||
|
||||
@@ -337,8 +348,8 @@ public:
|
||||
|
||||
~ReportingETL()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "onStop called";
|
||||
BOOST_LOG_TRIVIAL(debug) << "Stopping Reporting ETL";
|
||||
log_.info() << "onStop called";
|
||||
log_.debug() << "Stopping Reporting ETL";
|
||||
stopping_ = true;
|
||||
|
||||
if (worker_.joinable())
|
||||
@@ -346,7 +357,7 @@ public:
|
||||
if (cacheDownloader_.joinable())
|
||||
cacheDownloader_.join();
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Joined ReportingETL worker thread";
|
||||
log_.debug() << "Joined ReportingETL worker thread";
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
@@ -359,8 +370,7 @@ public:
|
||||
result["read_only"] = readOnly_;
|
||||
auto last = getLastPublish();
|
||||
if (last.time_since_epoch().count() != 0)
|
||||
result["last_publish_age_seconds"] =
|
||||
std::to_string(lastPublishAgeSeconds());
|
||||
result["last_publish_age_seconds"] = std::to_string(lastPublishAgeSeconds());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -374,8 +384,7 @@ public:
|
||||
std::uint32_t
|
||||
lastPublishAgeSeconds() const
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now() - getLastPublish())
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - getLastPublish())
|
||||
.count();
|
||||
}
|
||||
|
||||
@@ -383,8 +392,7 @@ public:
|
||||
lastCloseAgeSeconds() const
|
||||
{
|
||||
std::shared_lock lck(closeTimeMtx_);
|
||||
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
auto now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
auto closeTime = lastCloseTime_.time_since_epoch().count();
|
||||
if (now < (rippleEpochStart + closeTime))
|
||||
@@ -392,5 +400,3 @@ public:
|
||||
return now - (rippleEpochStart + closeTime);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
192
src/log/Logger.cpp
Normal file
192
src/log/Logger.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
|
||||
namespace clio {
|
||||
|
||||
Logger LogService::general_log_ = Logger{"General"};
|
||||
Logger LogService::alert_log_ = Logger{"Alert"};
|
||||
|
||||
std::ostream&
|
||||
operator<<(std::ostream& stream, Severity sev)
|
||||
{
|
||||
static constexpr std::array<const char*, 6> labels = {
|
||||
"TRC",
|
||||
"DBG",
|
||||
"NFO",
|
||||
"WRN",
|
||||
"ERR",
|
||||
"FTL",
|
||||
};
|
||||
|
||||
return stream << labels.at(static_cast<int>(sev));
|
||||
}
|
||||
|
||||
Severity
|
||||
tag_invoke(boost::json::value_to_tag<Severity>, boost::json::value const& value)
|
||||
{
|
||||
if (not value.is_string())
|
||||
throw std::runtime_error("`log_level` must be a string");
|
||||
auto const& logLevel = value.as_string();
|
||||
|
||||
if (boost::iequals(logLevel, "trace"))
|
||||
return Severity::TRC;
|
||||
else if (boost::iequals(logLevel, "debug"))
|
||||
return Severity::DBG;
|
||||
else if (boost::iequals(logLevel, "info"))
|
||||
return Severity::NFO;
|
||||
else if (boost::iequals(logLevel, "warning") || boost::iequals(logLevel, "warn"))
|
||||
return Severity::WRN;
|
||||
else if (boost::iequals(logLevel, "error"))
|
||||
return Severity::ERR;
|
||||
else if (boost::iequals(logLevel, "fatal"))
|
||||
return Severity::FTL;
|
||||
else
|
||||
throw std::runtime_error(
|
||||
"Could not parse `log_level`: expected `trace`, `debug`, `info`, "
|
||||
"`warning`, `error` or `fatal`");
|
||||
}
|
||||
|
||||
void
|
||||
LogService::init(Config const& config)
|
||||
{
|
||||
namespace src = boost::log::sources;
|
||||
namespace keywords = boost::log::keywords;
|
||||
namespace sinks = boost::log::sinks;
|
||||
|
||||
boost::log::add_common_attributes();
|
||||
boost::log::register_simple_formatter_factory<Severity, char>("Severity");
|
||||
auto const defaultFormat =
|
||||
"%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% "
|
||||
"%Message%";
|
||||
std::string format = config.valueOr<std::string>("log_format", defaultFormat);
|
||||
|
||||
if (config.valueOr("log_to_console", false))
|
||||
{
|
||||
boost::log::add_console_log(std::cout, keywords::format = format);
|
||||
}
|
||||
|
||||
auto logDir = config.maybeValue<std::string>("log_directory");
|
||||
if (logDir)
|
||||
{
|
||||
boost::filesystem::path dirPath{logDir.value()};
|
||||
if (!boost::filesystem::exists(dirPath))
|
||||
boost::filesystem::create_directories(dirPath);
|
||||
auto const rotationSize = config.valueOr<uint64_t>("log_rotation_size", 2048u) * 1024u * 1024u;
|
||||
auto const rotationPeriod = config.valueOr<uint32_t>("log_rotation_hour_interval", 12u);
|
||||
auto const dirSize = config.valueOr<uint64_t>("log_directory_max_size", 50u * 1024u) * 1024u * 1024u;
|
||||
auto fileSink = boost::log::add_file_log(
|
||||
keywords::file_name = dirPath / "clio.log",
|
||||
keywords::target_file_name = dirPath / "clio_%Y-%m-%d_%H-%M-%S.log",
|
||||
keywords::auto_flush = true,
|
||||
keywords::format = format,
|
||||
keywords::open_mode = std::ios_base::app,
|
||||
keywords::rotation_size = rotationSize,
|
||||
keywords::time_based_rotation =
|
||||
sinks::file::rotation_at_time_interval(boost::posix_time::hours(rotationPeriod)));
|
||||
fileSink->locked_backend()->set_file_collector(
|
||||
sinks::file::make_collector(keywords::target = dirPath, keywords::max_size = dirSize));
|
||||
fileSink->locked_backend()->scan_for_files();
|
||||
}
|
||||
|
||||
// get default severity, can be overridden per channel using
|
||||
// the `log_channels` array
|
||||
auto defaultSeverity = config.valueOr<Severity>("log_level", Severity::NFO);
|
||||
static constexpr std::array<const char*, 7> channels = {
|
||||
"General",
|
||||
"WebServer",
|
||||
"Backend",
|
||||
"RPC",
|
||||
"ETL",
|
||||
"Subscriptions",
|
||||
"Performance",
|
||||
};
|
||||
|
||||
auto core = boost::log::core::get();
|
||||
auto min_severity = boost::log::expressions::channel_severity_filter(log_channel, log_severity);
|
||||
|
||||
for (auto const& channel : channels)
|
||||
min_severity[channel] = defaultSeverity;
|
||||
min_severity["Alert"] = Severity::WRN; // Channel for alerts, always warning severity
|
||||
|
||||
for (auto const overrides = config.arrayOr("log_channels", {}); auto const& cfg : overrides)
|
||||
{
|
||||
auto name = cfg.valueOrThrow<std::string>("channel", "Channel name is required");
|
||||
if (not std::count(std::begin(channels), std::end(channels), name))
|
||||
throw std::runtime_error("Can't override settings for log channel " + name + ": invalid channel");
|
||||
|
||||
min_severity[name] = cfg.valueOr<Severity>("log_level", defaultSeverity);
|
||||
}
|
||||
|
||||
core->set_filter(min_severity);
|
||||
LogService::info() << "Default log level = " << defaultSeverity;
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
Logger::trace(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::TRC, loc};
|
||||
};
|
||||
Logger::Pump
|
||||
Logger::debug(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::DBG, loc};
|
||||
};
|
||||
Logger::Pump
|
||||
Logger::info(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::NFO, loc};
|
||||
};
|
||||
Logger::Pump
|
||||
Logger::warn(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::WRN, loc};
|
||||
};
|
||||
Logger::Pump
|
||||
Logger::error(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::ERR, loc};
|
||||
};
|
||||
Logger::Pump
|
||||
Logger::fatal(source_location_t const& loc) const
|
||||
{
|
||||
return {logger_, Severity::FTL, loc};
|
||||
};
|
||||
|
||||
std::string
|
||||
Logger::Pump::pretty_path(source_location_t const& loc, size_t max_depth) const
|
||||
{
|
||||
auto const file_path = std::string{loc.file_name()};
|
||||
auto idx = file_path.size();
|
||||
while (max_depth-- > 0)
|
||||
{
|
||||
idx = file_path.rfind('/', idx - 1);
|
||||
if (idx == std::string::npos || idx == 0)
|
||||
break;
|
||||
}
|
||||
return file_path.substr(idx == std::string::npos ? 0 : idx + 1) + ':' + std::to_string(loc.line());
|
||||
}
|
||||
|
||||
} // namespace clio
|
||||
306
src/log/Logger.h
Normal file
306
src/log/Logger.h
Normal file
@@ -0,0 +1,306 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/core/core.hpp>
|
||||
#include <boost/log/expressions/predicates/channel_severity_filter.hpp>
|
||||
#include <boost/log/sinks/unlocked_frontend.hpp>
|
||||
#include <boost/log/sources/record_ostream.hpp>
|
||||
#include <boost/log/sources/severity_channel_logger.hpp>
|
||||
#include <boost/log/sources/severity_feature.hpp>
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
#include <boost/log/utility/manipulators/add_value.hpp>
|
||||
#include <boost/log/utility/setup/common_attributes.hpp>
|
||||
#include <boost/log/utility/setup/console.hpp>
|
||||
#include <boost/log/utility/setup/file.hpp>
|
||||
#include <boost/log/utility/setup/formatter_parser.hpp>
|
||||
|
||||
#if defined(HAS_SOURCE_LOCATION) && __has_builtin(__builtin_source_location)
|
||||
// this is used by fully compatible compilers like gcc
|
||||
#include <source_location>
|
||||
|
||||
#elif defined(HAS_EXPERIMENTAL_SOURCE_LOCATION)
|
||||
// this is used by clang on linux where source_location is still not out of
|
||||
// experimental headers
|
||||
#include <experimental/source_location>
|
||||
#endif
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace clio {
|
||||
|
||||
class Config;
|
||||
#if defined(HAS_SOURCE_LOCATION) && __has_builtin(__builtin_source_location)
|
||||
using source_location_t = std::source_location;
|
||||
#define CURRENT_SRC_LOCATION source_location_t::current()
|
||||
|
||||
#elif defined(HAS_EXPERIMENTAL_SOURCE_LOCATION)
|
||||
using source_location_t = std::experimental::source_location;
|
||||
#define CURRENT_SRC_LOCATION source_location_t::current()
|
||||
|
||||
#else
|
||||
// A workaround for AppleClang that is lacking source_location atm.
|
||||
// TODO: remove this workaround when all compilers catch up to c++20
|
||||
class SourceLocation
|
||||
{
|
||||
std::string_view file_;
|
||||
std::size_t line_;
|
||||
|
||||
public:
|
||||
SourceLocation(std::string_view file, std::size_t line) : file_{file}, line_{line}
|
||||
{
|
||||
}
|
||||
std::string_view
|
||||
file_name() const
|
||||
{
|
||||
return file_;
|
||||
}
|
||||
std::size_t
|
||||
line() const
|
||||
{
|
||||
return line_;
|
||||
}
|
||||
};
|
||||
using source_location_t = SourceLocation;
|
||||
#define CURRENT_SRC_LOCATION source_location_t(__builtin_FILE(), __builtin_LINE())
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Custom severity levels for @ref Logger.
|
||||
*/
|
||||
enum class Severity {
|
||||
TRC,
|
||||
DBG,
|
||||
NFO,
|
||||
WRN,
|
||||
ERR,
|
||||
FTL,
|
||||
};
|
||||
|
||||
BOOST_LOG_ATTRIBUTE_KEYWORD(log_severity, "Severity", Severity);
|
||||
BOOST_LOG_ATTRIBUTE_KEYWORD(log_channel, "Channel", std::string);
|
||||
|
||||
/**
|
||||
* @brief Custom labels for @ref Severity in log output.
|
||||
*
|
||||
* @param stream std::ostream The output stream
|
||||
* @param sev Severity The severity to output to the ostream
|
||||
* @return std::ostream& The same ostream we were given
|
||||
*/
|
||||
std::ostream&
|
||||
operator<<(std::ostream& stream, Severity sev);
|
||||
|
||||
/**
|
||||
* @brief A simple thread-safe logger for the channel specified
|
||||
* in the constructor.
|
||||
*
|
||||
* This is cheap to copy and move. Designed to be used as a member variable or
|
||||
* otherwise. See @ref LogService::init() for setup of the logging core and
|
||||
* severity levels for each channel.
|
||||
*/
|
||||
class Logger final
|
||||
{
|
||||
using logger_t = boost::log::sources::severity_channel_logger_mt<Severity, std::string>;
|
||||
mutable logger_t logger_;
|
||||
|
||||
friend class LogService; // to expose the Pump interface
|
||||
|
||||
/**
|
||||
* @brief Helper that pumps data into a log record via `operator<<`.
|
||||
*/
|
||||
class Pump final
|
||||
{
|
||||
using pump_opt_t = std::optional<boost::log::aux::record_pump<logger_t>>;
|
||||
|
||||
boost::log::record rec_;
|
||||
pump_opt_t pump_ = std::nullopt;
|
||||
|
||||
public:
|
||||
~Pump() = default;
|
||||
Pump(logger_t& logger, Severity sev, source_location_t const& loc)
|
||||
: rec_{logger.open_record(boost::log::keywords::severity = sev)}
|
||||
{
|
||||
if (rec_)
|
||||
{
|
||||
pump_.emplace(boost::log::aux::make_record_pump(logger, rec_));
|
||||
pump_->stream() << boost::log::add_value("SourceLocation", pretty_path(loc));
|
||||
}
|
||||
}
|
||||
|
||||
Pump(Pump&&) = delete;
|
||||
Pump(Pump const&) = delete;
|
||||
Pump&
|
||||
operator=(Pump const&) = delete;
|
||||
Pump&
|
||||
operator=(Pump&&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Perfectly forwards any incoming data into the underlying
|
||||
* boost::log pump if the pump is available. nop otherwise.
|
||||
*
|
||||
* @tparam T Type of data to pump
|
||||
* @param data The data to pump
|
||||
* @return Pump& Reference to itself for chaining
|
||||
*/
|
||||
template <typename T>
|
||||
[[maybe_unused]] Pump&
|
||||
operator<<(T&& data)
|
||||
{
|
||||
if (pump_)
|
||||
pump_->stream() << std::forward<T>(data);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::string
|
||||
pretty_path(source_location_t const& loc, size_t max_depth = 3) const;
|
||||
|
||||
/**
|
||||
* @brief Custom JSON parser for @ref Severity.
|
||||
*
|
||||
* @param value The JSON string to parse
|
||||
* @return Severity The parsed severity
|
||||
* @throws std::runtime_error Thrown if severity is not in the right format
|
||||
*/
|
||||
friend Severity
|
||||
tag_invoke(boost::json::value_to_tag<Severity>, boost::json::value const& value);
|
||||
};
|
||||
|
||||
public:
|
||||
~Logger() = default;
|
||||
/**
|
||||
* @brief Construct a new Logger object that produces loglines for the
|
||||
* specified channel.
|
||||
*
|
||||
* See @ref LogService::init() for general setup and configuration of
|
||||
* severity levels per channel.
|
||||
*
|
||||
* @param channel The channel this logger will report into.
|
||||
*/
|
||||
Logger(std::string channel) : logger_{boost::log::keywords::channel = channel}
|
||||
{
|
||||
}
|
||||
Logger(Logger const&) = default;
|
||||
Logger(Logger&&) = default;
|
||||
Logger&
|
||||
operator=(Logger const&) = default;
|
||||
Logger&
|
||||
operator=(Logger&&) = default;
|
||||
|
||||
/*! Interface for logging at @ref Severity::TRC severity */
|
||||
[[nodiscard]] Pump
|
||||
trace(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
|
||||
/*! Interface for logging at @ref Severity::DBG severity */
|
||||
[[nodiscard]] Pump
|
||||
debug(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
|
||||
/*! Interface for logging at @ref Severity::INFO severity */
|
||||
[[nodiscard]] Pump
|
||||
info(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
|
||||
/*! Interface for logging at @ref Severity::WRN severity */
|
||||
[[nodiscard]] Pump
|
||||
warn(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
|
||||
/*! Interface for logging at @ref Severity::ERR severity */
|
||||
[[nodiscard]] Pump
|
||||
error(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
|
||||
/*! Interface for logging at @ref Severity::FTL severity */
|
||||
[[nodiscard]] Pump
|
||||
fatal(source_location_t const& loc = CURRENT_SRC_LOCATION) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A global logging service.
|
||||
*
|
||||
* Used to initialize and setup the logging core as well as a globally available
|
||||
* entrypoint for logging into the `General` channel as well as raising alerts.
|
||||
*/
|
||||
class LogService
|
||||
{
|
||||
static Logger general_log_; /*! Global logger for General channel */
|
||||
static Logger alert_log_; /*! Global logger for Alerts channel */
|
||||
|
||||
public:
|
||||
LogService() = delete;
|
||||
|
||||
/**
|
||||
* @brief Global log core initialization from a @ref Config
|
||||
*/
|
||||
static void
|
||||
init(Config const& config);
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::TRC severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
trace(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.trace(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::DBG severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
debug(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.debug(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::NFO severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
info(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.info(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::WRN severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
warn(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.warn(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::ERR severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
error(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.error(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible General logger at @ref Severity::FTL severity */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
fatal(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return general_log_.fatal(loc);
|
||||
}
|
||||
|
||||
/*! Globally accesible Alert logger */
|
||||
[[nodiscard]] static Logger::Pump
|
||||
alert(source_location_t const& loc = CURRENT_SRC_LOCATION)
|
||||
{
|
||||
return alert_log_.warn(loc);
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace clio
|
||||
@@ -1,5 +1,23 @@
|
||||
#ifndef CLIO_BUILD_INFO_H
|
||||
#define CLIO_BUILD_INFO_H
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -12,5 +30,3 @@ std::string const&
|
||||
getClioFullVersionString();
|
||||
|
||||
} // namespace Build
|
||||
|
||||
#endif // CLIO_BUILD_INFO_H
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#include <ripple/beast/core/SemanticVersion.h>
|
||||
#include <boost/preprocessor/stringize.hpp>
|
||||
#include <algorithm>
|
||||
#include <main/Build.h>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Build {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// The build version number. You must edit this for each release
|
||||
// and follow the format described at http://semver.org/
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
char const* const versionString = "1.0.2"
|
||||
// clang-format on
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZER)
|
||||
"+"
|
||||
#ifdef CLIO_GIT_COMMIT_HASH
|
||||
CLIO_GIT_COMMIT_HASH
|
||||
"."
|
||||
#endif
|
||||
#ifdef DEBUG
|
||||
"DEBUG"
|
||||
#ifdef SANITIZER
|
||||
"."
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef SANITIZER
|
||||
BOOST_PP_STRINGIZE(SANITIZER)
|
||||
#endif
|
||||
#endif
|
||||
#ifdef PKG
|
||||
"-release"
|
||||
#endif
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
;
|
||||
|
||||
std::string const&
|
||||
getClioVersionString()
|
||||
{
|
||||
static std::string const value = [] {
|
||||
std::string const s = versionString;
|
||||
beast::SemanticVersion v;
|
||||
if (!v.parse(s) || v.print() != s)
|
||||
throw std::runtime_error(s + ": Bad server version string");
|
||||
return s;
|
||||
}();
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
getClioFullVersionString()
|
||||
{
|
||||
static std::string const value = "clio-" + getClioVersionString();
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace Build
|
||||
@@ -1,3 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022-2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <grpc/impl/codegen/port_platform.h>
|
||||
#ifdef GRPC_TSAN_ENABLED
|
||||
#undef GRPC_TSAN_ENABLED
|
||||
@@ -6,26 +25,25 @@
|
||||
#undef GRPC_ASAN_ENABLED
|
||||
#endif
|
||||
|
||||
#include <backend/BackendFactory.h>
|
||||
#include <config/Config.h>
|
||||
#include <etl/ReportingETL.h>
|
||||
#include <log/Logger.h>
|
||||
#include <rpc/Counters.h>
|
||||
#include <rpc/RPCEngine.h>
|
||||
#include <rpc/common/impl/HandlerProvider.h>
|
||||
#include <webserver/Listener.h>
|
||||
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_types.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/core.hpp>
|
||||
#include <boost/log/expressions.hpp>
|
||||
#include <boost/log/sinks/text_file_backend.hpp>
|
||||
#include <boost/log/sources/record_ostream.hpp>
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
#include <boost/log/support/date_time.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/log/utility/setup/common_attributes.hpp>
|
||||
#include <boost/log/utility/setup/console.hpp>
|
||||
#include <boost/log/utility/setup/file.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <backend/BackendFactory.h>
|
||||
#include <config/Config.h>
|
||||
#include <cstdlib>
|
||||
#include <etl/ReportingETL.h>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
@@ -35,12 +53,60 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <webserver/Listener.h>
|
||||
|
||||
using namespace clio;
|
||||
namespace po = boost::program_options;
|
||||
|
||||
/**
|
||||
* @brief Parse command line and return path to configuration file
|
||||
*
|
||||
* @param argc
|
||||
* @param argv
|
||||
* @return std::string Path to configuration file
|
||||
*/
|
||||
std::string
|
||||
parseCli(int argc, char* argv[])
|
||||
{
|
||||
static constexpr char defaultConfigPath[] = "/etc/opt/clio/config.json";
|
||||
|
||||
// clang-format off
|
||||
po::options_description description("Options");
|
||||
description.add_options()
|
||||
("help,h", "print help message and exit")
|
||||
("version,v", "print version and exit")
|
||||
("conf,c", po::value<std::string>()->default_value(defaultConfigPath), "configuration file")
|
||||
;
|
||||
// clang-format on
|
||||
po::positional_options_description positional;
|
||||
positional.add("conf", 1);
|
||||
|
||||
po::variables_map parsed;
|
||||
po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed);
|
||||
po::notify(parsed);
|
||||
|
||||
if (parsed.count("version"))
|
||||
{
|
||||
std::cout << Build::getClioFullVersionString() << '\n';
|
||||
std::exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
if (parsed.count("help"))
|
||||
{
|
||||
std::cout << "Clio server " << Build::getClioFullVersionString() << "\n\n" << description;
|
||||
std::exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
return parsed["conf"].as<std::string>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse certificates from configuration file
|
||||
*
|
||||
* @param config The configuration
|
||||
* @return std::optional<ssl::context> SSL context if certificates were parsed
|
||||
*/
|
||||
std::optional<ssl::context>
|
||||
parse_certs(Config const& config)
|
||||
parseCerts(Config const& config)
|
||||
{
|
||||
if (!config.contains("ssl_cert_file") || !config.contains("ssl_key_file"))
|
||||
return {};
|
||||
@@ -66,99 +132,19 @@ parse_certs(Config const& config)
|
||||
std::string key = contents.str();
|
||||
|
||||
ssl::context ctx{ssl::context::tlsv12};
|
||||
|
||||
ctx.set_options(
|
||||
boost::asio::ssl::context::default_workarounds |
|
||||
boost::asio::ssl::context::no_sslv2);
|
||||
|
||||
ctx.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2);
|
||||
ctx.use_certificate_chain(boost::asio::buffer(cert.data(), cert.size()));
|
||||
|
||||
ctx.use_private_key(
|
||||
boost::asio::buffer(key.data(), key.size()),
|
||||
boost::asio::ssl::context::file_format::pem);
|
||||
ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context::file_format::pem);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void
|
||||
initLogging(Config const& config)
|
||||
{
|
||||
namespace src = boost::log::sources;
|
||||
namespace keywords = boost::log::keywords;
|
||||
namespace sinks = boost::log::sinks;
|
||||
namespace trivial = boost::log::trivial;
|
||||
boost::log::add_common_attributes();
|
||||
std::string format = "[%TimeStamp%] [%ThreadID%] [%Severity%] %Message%";
|
||||
if (config.valueOr<bool>("log_to_console", true))
|
||||
{
|
||||
boost::log::add_console_log(std::cout, keywords::format = format);
|
||||
}
|
||||
|
||||
if (auto logDir = config.maybeValue<std::string>("log_directory"); logDir)
|
||||
{
|
||||
boost::filesystem::path dirPath{*logDir};
|
||||
if (!boost::filesystem::exists(dirPath))
|
||||
boost::filesystem::create_directories(dirPath);
|
||||
auto const rotationSize =
|
||||
config.valueOr<int64_t>("log_rotation_size", 2 * 1024) * 1024 *
|
||||
1024u;
|
||||
if (rotationSize <= 0)
|
||||
throw std::runtime_error(
|
||||
"log rotation size must be greater than 0");
|
||||
auto const rotationPeriod =
|
||||
config.valueOr<int64_t>("log_rotation_hour_interval", 12u);
|
||||
if (rotationPeriod <= 0)
|
||||
throw std::runtime_error(
|
||||
"log rotation time interval must be greater than 0");
|
||||
auto const dirSize =
|
||||
config.valueOr<int64_t>("log_directory_max_size", 50 * 1024) *
|
||||
1024 * 1024u;
|
||||
if (dirSize <= 0)
|
||||
throw std::runtime_error(
|
||||
"log rotation directory max size must be greater than 0");
|
||||
auto fileSink = boost::log::add_file_log(
|
||||
keywords::file_name = dirPath / "clio.log",
|
||||
keywords::target_file_name = dirPath / "clio_%Y-%m-%d_%H-%M-%S.log",
|
||||
keywords::auto_flush = true,
|
||||
keywords::format = format,
|
||||
keywords::open_mode = std::ios_base::app,
|
||||
keywords::rotation_size = rotationSize,
|
||||
keywords::time_based_rotation =
|
||||
sinks::file::rotation_at_time_interval(
|
||||
boost::posix_time::hours(rotationPeriod)));
|
||||
fileSink->locked_backend()->set_file_collector(
|
||||
sinks::file::make_collector(
|
||||
keywords::target = dirPath, keywords::max_size = dirSize));
|
||||
fileSink->locked_backend()->scan_for_files();
|
||||
}
|
||||
auto const logLevel = config.valueOr<std::string>("log_level", "info");
|
||||
if (boost::iequals(logLevel, "trace"))
|
||||
boost::log::core::get()->set_filter(
|
||||
trivial::severity >= trivial::trace);
|
||||
else if (boost::iequals(logLevel, "debug"))
|
||||
boost::log::core::get()->set_filter(
|
||||
trivial::severity >= trivial::debug);
|
||||
else if (boost::iequals(logLevel, "info"))
|
||||
boost::log::core::get()->set_filter(trivial::severity >= trivial::info);
|
||||
else if (
|
||||
boost::iequals(logLevel, "warning") || boost::iequals(logLevel, "warn"))
|
||||
boost::log::core::get()->set_filter(
|
||||
trivial::severity >= trivial::warning);
|
||||
else if (boost::iequals(logLevel, "error"))
|
||||
boost::log::core::get()->set_filter(
|
||||
trivial::severity >= trivial::error);
|
||||
else if (boost::iequals(logLevel, "fatal"))
|
||||
boost::log::core::get()->set_filter(
|
||||
trivial::severity >= trivial::fatal);
|
||||
else
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "Unrecognized log level: " << logLevel
|
||||
<< ". Setting log level to info";
|
||||
boost::log::core::get()->set_filter(trivial::severity >= trivial::info);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Log level = " << logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start context threads
|
||||
*
|
||||
* @param ioc Context
|
||||
* @param numThreads Number of worker threads to start
|
||||
*/
|
||||
void
|
||||
start(boost::asio::io_context& ioc, std::uint32_t numThreads)
|
||||
{
|
||||
@@ -172,84 +158,69 @@ start(boost::asio::io_context& ioc, std::uint32_t numThreads)
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
try
|
||||
{
|
||||
// Check command line arguments.
|
||||
if (argc != 2)
|
||||
{
|
||||
std::cerr << "Usage: clio_server "
|
||||
"<config_file> \n"
|
||||
<< "Example:\n"
|
||||
<< " clio_server config.json \n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (std::string{argv[1]} == "-v" || std::string{argv[1]} == "--version")
|
||||
{
|
||||
std::cout << Build::getClioFullVersionString() << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
auto const config = ConfigReader::open(argv[1]);
|
||||
auto const configPath = parseCli(argc, argv);
|
||||
auto const config = ConfigReader::open(configPath);
|
||||
if (!config)
|
||||
{
|
||||
std::cerr << "Couldnt parse config. Exiting..." << std::endl;
|
||||
std::cerr << "Couldnt parse config '" << configPath << "'." << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
initLogging(config);
|
||||
LogService::init(config);
|
||||
LogService::info() << "Clio version: " << Build::getClioFullVersionString();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "Clio version: " << Build::getClioFullVersionString();
|
||||
|
||||
auto ctx = parse_certs(config);
|
||||
auto ctxRef = ctx
|
||||
? std::optional<std::reference_wrapper<ssl::context>>{ctx.value()}
|
||||
: std::nullopt;
|
||||
auto ctx = parseCerts(config);
|
||||
auto ctxRef = ctx ? std::optional<std::reference_wrapper<ssl::context>>{ctx.value()} : std::nullopt;
|
||||
|
||||
auto const threads = config.valueOr("io_threads", 2);
|
||||
if (threads <= 0)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(fatal) << "io_threads is less than 0";
|
||||
LogService::fatal() << "io_threads is less than 0";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Number of io threads = " << threads;
|
||||
LogService::info() << "Number of io threads = " << threads;
|
||||
|
||||
// io context to handle all incoming requests, as well as other things
|
||||
// IO context to handle all incoming requests, as well as other things
|
||||
// This is not the only io context in the application
|
||||
boost::asio::io_context ioc{threads};
|
||||
|
||||
// Rate limiter, to prevent abuse
|
||||
DOSGuard dosGuard{config, ioc};
|
||||
auto sweepHandler = IntervalSweepHandler{config, ioc};
|
||||
auto dosGuard = DOSGuard{config, sweepHandler};
|
||||
|
||||
// Interface to the database
|
||||
std::shared_ptr<BackendInterface> backend{
|
||||
Backend::make_Backend(ioc, config)};
|
||||
auto backend = Backend::make_Backend(ioc, config);
|
||||
|
||||
// Manages clients subscribed to streams
|
||||
std::shared_ptr<SubscriptionManager> subscriptions{
|
||||
SubscriptionManager::make_SubscriptionManager(config, backend)};
|
||||
auto subscriptions = SubscriptionManager::make_SubscriptionManager(config, backend);
|
||||
|
||||
// Tracks which ledgers have been validated by the
|
||||
// network
|
||||
std::shared_ptr<NetworkValidatedLedgers> ledgers{
|
||||
NetworkValidatedLedgers::make_ValidatedLedgers()};
|
||||
auto ledgers = NetworkValidatedLedgers::make_ValidatedLedgers();
|
||||
|
||||
// Handles the connection to one or more rippled nodes.
|
||||
// ETL uses the balancer to extract data.
|
||||
// The server uses the balancer to forward RPCs to a rippled node.
|
||||
// The balancer itself publishes to streams (transactions_proposed and
|
||||
// accounts_proposed)
|
||||
auto balancer = ETLLoadBalancer::make_ETLLoadBalancer(
|
||||
config, ioc, backend, subscriptions, ledgers);
|
||||
auto balancer = ETLLoadBalancer::make_ETLLoadBalancer(config, ioc, backend, subscriptions, ledgers);
|
||||
|
||||
// ETL is responsible for writing and publishing to streams. In read-only
|
||||
// mode, ETL only publishes
|
||||
auto etl = ReportingETL::make_ReportingETL(
|
||||
config, ioc, backend, subscriptions, balancer, ledgers);
|
||||
auto etl = ReportingETL::make_ReportingETL(config, ioc, backend, subscriptions, balancer, ledgers);
|
||||
|
||||
auto workQueue = WorkQueue::make_WorkQueue(config);
|
||||
auto counters = RPC::Counters::make_Counters(workQueue);
|
||||
auto const handlerProvider =
|
||||
std::make_shared<RPC::detail::ProductionHandlerProvider const>(backend, subscriptions, balancer, etl, counters);
|
||||
auto const rpcEngine = RPC::RPCEngine::make_RPCEngine(
|
||||
config, backend, subscriptions, balancer, etl, dosGuard, workQueue, counters, handlerProvider);
|
||||
|
||||
// The server handles incoming RPCs
|
||||
auto httpServer = Server::make_HttpServer(
|
||||
config, ioc, ctxRef, backend, subscriptions, balancer, etl, dosGuard);
|
||||
auto httpServer =
|
||||
Server::make_HttpServer(config, ioc, ctxRef, backend, rpcEngine, subscriptions, balancer, etl, dosGuard);
|
||||
|
||||
// Blocks until stopped.
|
||||
// When stopped, shared_ptrs fall out of scope
|
||||
@@ -258,3 +229,7 @@ main(int argc, char* argv[])
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
LogService::fatal() << "Exit on exception: " << e.what();
|
||||
}
|
||||
|
||||
235
src/rpc/BookChangesHelper.h
Normal file
235
src/rpc/BookChangesHelper.h
Normal file
@@ -0,0 +1,235 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/RPCHelpers.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief Represents an entry in the book_changes' changes array.
|
||||
*/
|
||||
struct BookChange
|
||||
{
|
||||
ripple::STAmount sideAVolume;
|
||||
ripple::STAmount sideBVolume;
|
||||
ripple::STAmount highRate;
|
||||
ripple::STAmount lowRate;
|
||||
ripple::STAmount openRate;
|
||||
ripple::STAmount closeRate;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Encapsulates the book_changes computations and transformations.
|
||||
*/
|
||||
class BookChanges final
|
||||
{
|
||||
public:
|
||||
BookChanges() = delete; // only accessed via static handle function
|
||||
|
||||
/**
|
||||
* @brief Computes all book_changes for the given transactions.
|
||||
*
|
||||
* @param transactions The transactions to compute book changes for
|
||||
* @return std::vector<BookChange> Book changes
|
||||
*/
|
||||
[[nodiscard]] static std::vector<BookChange>
|
||||
compute(std::vector<Backend::TransactionAndMetadata> const& transactions)
|
||||
{
|
||||
return HandlerImpl{}(transactions);
|
||||
}
|
||||
|
||||
private:
|
||||
class HandlerImpl final
|
||||
{
|
||||
std::map<std::string, BookChange> tally_ = {};
|
||||
std::optional<uint32_t> offerCancel_ = {};
|
||||
|
||||
public:
|
||||
[[nodiscard]] std::vector<BookChange>
|
||||
operator()(std::vector<Backend::TransactionAndMetadata> const& transactions)
|
||||
{
|
||||
for (auto const& tx : transactions)
|
||||
handleBookChange(tx);
|
||||
|
||||
// TODO: rewrite this with std::ranges when compilers catch up
|
||||
std::vector<BookChange> changes;
|
||||
std::transform(
|
||||
std::make_move_iterator(std::begin(tally_)),
|
||||
std::make_move_iterator(std::end(tally_)),
|
||||
std::back_inserter(changes),
|
||||
[](auto obj) { return obj.second; });
|
||||
return changes;
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
handleAffectedNode(ripple::STObject const& node)
|
||||
{
|
||||
auto const& metaType = node.getFName();
|
||||
auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType);
|
||||
|
||||
// we only care about ripple::ltOFFER objects being modified or
|
||||
// deleted
|
||||
if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode)
|
||||
return;
|
||||
|
||||
// if either FF or PF are missing we can't compute
|
||||
// but generally these are cancelled rather than crossed
|
||||
// so skipping them is consistent
|
||||
if (!node.isFieldPresent(ripple::sfFinalFields) || !node.isFieldPresent(ripple::sfPreviousFields))
|
||||
return;
|
||||
|
||||
auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
|
||||
auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
|
||||
|
||||
// defensive case that should never be hit
|
||||
if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) ||
|
||||
!previousFields.isFieldPresent(ripple::sfTakerGets) ||
|
||||
!previousFields.isFieldPresent(ripple::sfTakerPays))
|
||||
return;
|
||||
|
||||
// filter out any offers deleted by explicit offer cancels
|
||||
if (metaType == ripple::sfDeletedNode && offerCancel_ &&
|
||||
finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
|
||||
return;
|
||||
|
||||
// compute the difference in gets and pays actually
|
||||
// affected onto the offer
|
||||
auto const deltaGets =
|
||||
finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets);
|
||||
auto const deltaPays =
|
||||
finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays);
|
||||
|
||||
transformAndStore(deltaGets, deltaPays);
|
||||
}
|
||||
|
||||
void
|
||||
transformAndStore(ripple::STAmount const& deltaGets, ripple::STAmount const& deltaPays)
|
||||
{
|
||||
auto const g = to_string(deltaGets.issue());
|
||||
auto const p = to_string(deltaPays.issue());
|
||||
|
||||
auto const noswap = isXRP(deltaGets) ? true : (isXRP(deltaPays) ? false : (g < p));
|
||||
|
||||
auto first = noswap ? deltaGets : deltaPays;
|
||||
auto second = noswap ? deltaPays : deltaGets;
|
||||
|
||||
// defensively programmed, should (probably) never happen
|
||||
if (second == beast::zero)
|
||||
return;
|
||||
|
||||
auto const rate = divide(first, second, ripple::noIssue());
|
||||
|
||||
if (first < beast::zero)
|
||||
first = -first;
|
||||
|
||||
if (second < beast::zero)
|
||||
second = -second;
|
||||
|
||||
auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
|
||||
if (tally_.contains(key))
|
||||
{
|
||||
auto& entry = tally_.at(key);
|
||||
|
||||
entry.sideAVolume += first;
|
||||
entry.sideBVolume += second;
|
||||
|
||||
if (entry.highRate < rate)
|
||||
entry.highRate = rate;
|
||||
|
||||
if (entry.lowRate > rate)
|
||||
entry.lowRate = rate;
|
||||
|
||||
entry.closeRate = rate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: use paranthesized initialization when clang catches up
|
||||
tally_[key] = {
|
||||
first, // sideAVolume
|
||||
second, // sideBVolume
|
||||
rate, // highRate
|
||||
rate, // lowRate
|
||||
rate, // openRate
|
||||
rate, // closeRate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
handleBookChange(Backend::TransactionAndMetadata const& blob)
|
||||
{
|
||||
auto const [tx, meta] = RPC::deserializeTxPlusMeta(blob);
|
||||
if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
|
||||
return;
|
||||
|
||||
offerCancel_ = shouldCancelOffer(tx);
|
||||
for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
|
||||
handleAffectedNode(node);
|
||||
}
|
||||
|
||||
std::optional<uint32_t>
|
||||
shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx) const
|
||||
{
|
||||
switch (tx->getFieldU16(ripple::sfTransactionType))
|
||||
{
|
||||
// in future if any other ways emerge to cancel an offer
|
||||
// this switch makes them easy to add
|
||||
case ripple::ttOFFER_CANCEL:
|
||||
case ripple::ttOFFER_CREATE:
|
||||
if (tx->isFieldPresent(ripple::sfOfferSequence))
|
||||
return tx->getFieldU32(ripple::sfOfferSequence);
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
inline void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
|
||||
{
|
||||
auto amountStr = [](ripple::STAmount const& amount) -> std::string {
|
||||
return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
|
||||
};
|
||||
|
||||
auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
|
||||
return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
|
||||
};
|
||||
|
||||
jv = {
|
||||
{JS(currency_a), currencyStr(change.sideAVolume)},
|
||||
{JS(currency_b), currencyStr(change.sideBVolume)},
|
||||
{JS(volume_a), amountStr(change.sideAVolume)},
|
||||
{JS(volume_b), amountStr(change.sideBVolume)},
|
||||
{JS(high), to_string(change.highRate.iou())},
|
||||
{JS(low), to_string(change.lowRate.iou())},
|
||||
{JS(open), to_string(change.openRate.iou())},
|
||||
{JS(close), to_string(change.closeRate.iou())},
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] boost::json::object const
|
||||
computeBookChanges(ripple::LedgerInfo const& lgrInfo, std::vector<Backend::TransactionAndMetadata> const& transactions);
|
||||
|
||||
} // namespace RPC
|
||||
@@ -1,48 +1,41 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <rpc/Counters.h>
|
||||
#include <rpc/RPC.h>
|
||||
#include <rpc/JS.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
void
|
||||
Counters::initializeCounter(std::string const& method)
|
||||
{
|
||||
std::shared_lock lk(mutex_);
|
||||
if (methodInfo_.count(method) == 0)
|
||||
{
|
||||
lk.unlock();
|
||||
std::unique_lock ulk(mutex_);
|
||||
|
||||
// This calls the default constructor for methodInfo of the method.
|
||||
methodInfo_[method];
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcErrored(std::string const& method)
|
||||
{
|
||||
if (!validHandler(method))
|
||||
return;
|
||||
|
||||
initializeCounter(method);
|
||||
|
||||
std::shared_lock lk(mutex_);
|
||||
std::scoped_lock lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
counters.started++;
|
||||
counters.errored++;
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcComplete(
|
||||
std::string const& method,
|
||||
std::chrono::microseconds const& rpcDuration)
|
||||
Counters::rpcComplete(std::string const& method, std::chrono::microseconds const& rpcDuration)
|
||||
{
|
||||
if (!validHandler(method))
|
||||
return;
|
||||
|
||||
initializeCounter(method);
|
||||
|
||||
std::shared_lock lk(mutex_);
|
||||
std::scoped_lock lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
counters.started++;
|
||||
counters.finished++;
|
||||
@@ -52,27 +45,23 @@ Counters::rpcComplete(
|
||||
void
|
||||
Counters::rpcForwarded(std::string const& method)
|
||||
{
|
||||
if (!validHandler(method))
|
||||
return;
|
||||
|
||||
initializeCounter(method);
|
||||
|
||||
std::shared_lock lk(mutex_);
|
||||
std::scoped_lock lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
counters.forwarded++;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
Counters::report()
|
||||
Counters::report() const
|
||||
{
|
||||
std::shared_lock lk(mutex_);
|
||||
boost::json::object obj = {};
|
||||
std::scoped_lock lk(mutex_);
|
||||
auto obj = boost::json::object{};
|
||||
|
||||
obj[JS(rpc)] = boost::json::object{};
|
||||
auto& rpc = obj[JS(rpc)].as_object();
|
||||
|
||||
for (auto const& [method, info] : methodInfo_)
|
||||
{
|
||||
boost::json::object counters = {};
|
||||
auto counters = boost::json::object{};
|
||||
counters[JS(started)] = std::to_string(info.started);
|
||||
counters[JS(finished)] = std::to_string(info.finished);
|
||||
counters[JS(errored)] = std::to_string(info.errored);
|
||||
@@ -81,6 +70,7 @@ Counters::report()
|
||||
|
||||
rpc[method] = std::move(counters);
|
||||
}
|
||||
|
||||
obj["work_queue"] = workQueue_.get().report();
|
||||
|
||||
return obj;
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
#ifndef RPC_COUNTERS_H
|
||||
#define RPC_COUNTERS_H
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/WorkQueue.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <rpc/WorkQueue.h>
|
||||
#include <shared_mutex>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -14,22 +32,16 @@ namespace RPC {
|
||||
|
||||
class Counters
|
||||
{
|
||||
private:
|
||||
struct MethodInfo
|
||||
{
|
||||
MethodInfo() = default;
|
||||
|
||||
std::atomic_uint64_t started{0};
|
||||
std::atomic_uint64_t finished{0};
|
||||
std::atomic_uint64_t errored{0};
|
||||
std::atomic_uint64_t forwarded{0};
|
||||
std::atomic_uint64_t duration{0};
|
||||
std::uint64_t started = 0u;
|
||||
std::uint64_t finished = 0u;
|
||||
std::uint64_t errored = 0u;
|
||||
std::uint64_t forwarded = 0u;
|
||||
std::uint64_t duration = 0u;
|
||||
};
|
||||
|
||||
void
|
||||
initializeCounter(std::string const& method);
|
||||
|
||||
std::shared_mutex mutex_;
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, MethodInfo> methodInfo_;
|
||||
|
||||
std::reference_wrapper<const WorkQueue> workQueue_;
|
||||
@@ -37,21 +49,23 @@ private:
|
||||
public:
|
||||
Counters(WorkQueue const& wq) : workQueue_(std::cref(wq)){};
|
||||
|
||||
static Counters
|
||||
make_Counters(WorkQueue const& wq)
|
||||
{
|
||||
return Counters{wq};
|
||||
}
|
||||
|
||||
void
|
||||
rpcErrored(std::string const& method);
|
||||
|
||||
void
|
||||
rpcComplete(
|
||||
std::string const& method,
|
||||
std::chrono::microseconds const& rpcDuration);
|
||||
rpcComplete(std::string const& method, std::chrono::microseconds const& rpcDuration);
|
||||
|
||||
void
|
||||
rpcForwarded(std::string const& method);
|
||||
|
||||
boost::json::object
|
||||
report();
|
||||
report() const;
|
||||
};
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
#endif // RPC_COUNTERS_H
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <rpc/Errors.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -28,11 +47,11 @@ getWarningInfo(WarningCode code)
|
||||
"want to talk to rippled, include 'ledger_index':'current' in your "
|
||||
"request"},
|
||||
{warnRPC_OUTDATED, "This server may be out of date"},
|
||||
{warnRPC_RATE_LIMIT, "You are about to be rate limited"}};
|
||||
{warnRPC_RATE_LIMIT, "You are about to be rate limited"},
|
||||
};
|
||||
|
||||
auto matchByCode = [code](auto const& info) { return info.code == code; };
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode);
|
||||
it != end(infos))
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode); it != end(infos))
|
||||
return *it;
|
||||
|
||||
throw(out_of_range("Invalid WarningCode"));
|
||||
@@ -41,10 +60,12 @@ getWarningInfo(WarningCode code)
|
||||
boost::json::object
|
||||
makeWarning(WarningCode code)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto json = boost::json::object{};
|
||||
auto const& info = getWarningInfo(code);
|
||||
|
||||
json["id"] = code;
|
||||
json["message"] = static_cast<string>(info.message);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -52,24 +73,21 @@ ClioErrorInfo const&
|
||||
getErrorInfo(ClioError code)
|
||||
{
|
||||
constexpr static ClioErrorInfo infos[]{
|
||||
{ClioError::rpcMALFORMED_CURRENCY,
|
||||
"malformedCurrency",
|
||||
"Malformed currency."},
|
||||
{ClioError::rpcMALFORMED_CURRENCY, "malformedCurrency", "Malformed currency."},
|
||||
{ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."},
|
||||
{ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."},
|
||||
{ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."},
|
||||
};
|
||||
|
||||
auto matchByCode = [code](auto const& info) { return info.code == code; };
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode);
|
||||
it != end(infos))
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode); it != end(infos))
|
||||
return *it;
|
||||
|
||||
throw(out_of_range("Invalid error code"));
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(
|
||||
RippledError err,
|
||||
optional<string_view> customError,
|
||||
optional<string_view> customMessage)
|
||||
makeError(RippledError err, optional<string_view> customError, optional<string_view> customMessage)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto const& info = ripple::RPC::get_error_info(err);
|
||||
@@ -79,14 +97,12 @@ makeError(
|
||||
json["error_message"] = customMessage.value_or(info.message.c_str()).data();
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(
|
||||
ClioError err,
|
||||
optional<string_view> customError,
|
||||
optional<string_view> customMessage)
|
||||
makeError(ClioError err, optional<string_view> customError, optional<string_view> customMessage)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto const& info = getErrorInfo(err);
|
||||
@@ -96,40 +112,34 @@ makeError(
|
||||
json["error_message"] = customMessage.value_or(info.message).data();
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(Status const& status)
|
||||
{
|
||||
auto wrapOptional = [](string_view const& str) {
|
||||
return str.empty() ? nullopt : make_optional(str);
|
||||
};
|
||||
auto wrapOptional = [](string_view const& str) { return str.empty() ? nullopt : make_optional(str); };
|
||||
|
||||
return visit(
|
||||
auto res = visit(
|
||||
overloadSet{
|
||||
[&status, &wrapOptional](RippledError err) {
|
||||
if (err == ripple::rpcUNKNOWN)
|
||||
{
|
||||
return boost::json::object{
|
||||
{"error", status.message},
|
||||
{"type", "response"},
|
||||
{"status", "error"}};
|
||||
}
|
||||
return boost::json::object{{"error", status.message}, {"type", "response"}, {"status", "error"}};
|
||||
|
||||
return makeError(
|
||||
err,
|
||||
wrapOptional(status.error),
|
||||
wrapOptional(status.message));
|
||||
return makeError(err, wrapOptional(status.error), wrapOptional(status.message));
|
||||
},
|
||||
[&status, &wrapOptional](ClioError err) {
|
||||
return makeError(
|
||||
err,
|
||||
wrapOptional(status.error),
|
||||
wrapOptional(status.message));
|
||||
return makeError(err, wrapOptional(status.error), wrapOptional(status.message));
|
||||
},
|
||||
},
|
||||
status.code);
|
||||
|
||||
if (status.extraInfo)
|
||||
for (auto& [key, value] : status.extraInfo.value())
|
||||
res[key] = value;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
#ifndef REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
#define REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -17,6 +35,9 @@ namespace RPC {
|
||||
*/
|
||||
enum class ClioError {
|
||||
rpcMALFORMED_CURRENCY = 5000,
|
||||
rpcMALFORMED_REQUEST = 5001,
|
||||
rpcMALFORMED_OWNER = 5002,
|
||||
rpcMALFORMED_ADDRESS = 5003,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,25 +71,23 @@ struct Status
|
||||
CombinedError code = RippledError::rpcSUCCESS;
|
||||
std::string error = "";
|
||||
std::string message = "";
|
||||
std::optional<boost::json::object> extraInfo;
|
||||
|
||||
Status() = default;
|
||||
/* implicit */ Status(CombinedError code) : code(code){};
|
||||
Status(CombinedError code, boost::json::object&& extraInfo) : code(code), extraInfo(std::move(extraInfo)){};
|
||||
|
||||
// HACK. Some rippled handlers explicitly specify errors.
|
||||
// This means that we have to be able to duplicate this
|
||||
// functionality.
|
||||
explicit Status(std::string const& message)
|
||||
: code(ripple::rpcUNKNOWN), message(message)
|
||||
// This means that we have to be able to duplicate this functionality.
|
||||
explicit Status(std::string const& message) : code(ripple::rpcUNKNOWN), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
Status(CombinedError code, std::string message)
|
||||
: code(code), message(message)
|
||||
Status(CombinedError code, std::string message) : code(code), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
Status(CombinedError code, std::string error, std::string message)
|
||||
: code(code), error(error), message(message)
|
||||
Status(CombinedError code, std::string error, std::string message) : code(code), error(error), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -79,19 +98,45 @@ struct Status
|
||||
{
|
||||
if (auto err = std::get_if<RippledError>(&code))
|
||||
return *err != RippledError::rpcSUCCESS;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns true if the Status contains the desired @ref RippledError
|
||||
*
|
||||
* @param other The RippledError to match
|
||||
* @return bool true if status matches given error; false otherwise
|
||||
*/
|
||||
bool
|
||||
operator==(RippledError other) const
|
||||
{
|
||||
if (auto err = std::get_if<RippledError>(&code))
|
||||
return *err == other;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns true if the Status contains the desired @ref ClioError
|
||||
*
|
||||
* @param other The RippledError to match
|
||||
* @return bool true if status matches given error; false otherwise
|
||||
*/
|
||||
bool
|
||||
operator==(ClioError other) const
|
||||
{
|
||||
if (auto err = std::get_if<ClioError>(&code))
|
||||
return *err == other;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Warning codes that can be returned by clio.
|
||||
*/
|
||||
enum WarningCode {
|
||||
warnUNKNOWN = -1,
|
||||
warnRPC_CLIO = 2001,
|
||||
warnRPC_OUTDATED = 2002,
|
||||
warnRPC_RATE_LIMIT = 2003
|
||||
};
|
||||
enum WarningCode { warnUNKNOWN = -1, warnRPC_CLIO = 2001, warnRPC_OUTDATED = 2002, warnRPC_RATE_LIMIT = 2003 };
|
||||
|
||||
/**
|
||||
* @brief Holds information about a clio warning.
|
||||
@@ -99,8 +144,7 @@ enum WarningCode {
|
||||
struct WarningInfo
|
||||
{
|
||||
constexpr WarningInfo() = default;
|
||||
constexpr WarningInfo(WarningCode code, char const* message)
|
||||
: code(code), message(message)
|
||||
constexpr WarningInfo(WarningCode code, char const* message) : code(code), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -138,6 +182,7 @@ public:
|
||||
explicit AccountNotFoundError(std::string const& acct) : account(acct)
|
||||
{
|
||||
}
|
||||
|
||||
const char*
|
||||
what() const throw() override
|
||||
{
|
||||
@@ -202,5 +247,3 @@ makeError(
|
||||
std::optional<std::string_view> customMessage = std::nullopt);
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
#endif // REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
|
||||
88
src/rpc/Factories.cpp
Normal file
88
src/rpc/Factories.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <etl/ETLSource.h>
|
||||
#include <rpc/Factories.h>
|
||||
#include <rpc/common/impl/HandlerProvider.h>
|
||||
#include <webserver/HttpBase.h>
|
||||
#include <webserver/WsBase.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
using namespace clio;
|
||||
using namespace RPC;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
optional<Web::Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
shared_ptr<WsBase> const& session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
string const& clientIp)
|
||||
{
|
||||
boost::json::value commandValue = nullptr;
|
||||
if (!request.contains("command") && request.contains("method"))
|
||||
commandValue = request.at("method");
|
||||
else if (request.contains("command") && !request.contains("method"))
|
||||
commandValue = request.at("command");
|
||||
|
||||
if (!commandValue.is_string())
|
||||
return {};
|
||||
|
||||
string command = commandValue.as_string().c_str();
|
||||
return make_optional<Web::Context>(yc, command, 1, request, session, tagFactory, range, clientIp);
|
||||
}
|
||||
|
||||
optional<Web::Context>
|
||||
make_HttpContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
string const& clientIp)
|
||||
{
|
||||
if (!request.contains("method") || !request.at("method").is_string())
|
||||
return {};
|
||||
|
||||
string const& command = request.at("method").as_string().c_str();
|
||||
|
||||
if (command == "subscribe" || command == "unsubscribe")
|
||||
return {};
|
||||
|
||||
if (!request.at("params").is_array())
|
||||
return {};
|
||||
|
||||
boost::json::array const& array = request.at("params").as_array();
|
||||
|
||||
if (array.size() != 1)
|
||||
return {};
|
||||
|
||||
if (!array.at(0).is_object())
|
||||
return {};
|
||||
|
||||
return make_optional<Web::Context>(yc, command, 1, array.at(0).as_object(), nullptr, tagFactory, range, clientIp);
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
67
src/rpc/Factories.h
Normal file
67
src/rpc/Factories.h
Normal file
@@ -0,0 +1,67 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <log/Logger.h>
|
||||
#include <rpc/Errors.h>
|
||||
#include <webserver/Context.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
* This file contains various classes necessary for executing RPC handlers.
|
||||
* Context gives the handlers access to various other parts of the application Status is used to report errors.
|
||||
* And lastly, there are various functions for making Contexts, Statuses and serializing Status to JSON.
|
||||
* This file is meant to contain any class or function that code outside of the rpc folder needs to use. For helper
|
||||
* functions or classes used within the rpc folder, use RPCHelpers.h.
|
||||
*/
|
||||
|
||||
class WsBase;
|
||||
class SubscriptionManager;
|
||||
class ETLLoadBalancer;
|
||||
class ReportingETL;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
std::optional<Web::Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
std::string const& clientIp);
|
||||
|
||||
std::optional<Web::Context>
|
||||
make_HttpContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
std::string const& clientIp);
|
||||
|
||||
} // namespace RPC
|
||||
59
src/rpc/HandlerTable.h
Normal file
59
src/rpc/HandlerTable.h
Normal file
@@ -0,0 +1,59 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/AnyHandler.h>
|
||||
#include <rpc/common/Types.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
class HandlerTable
|
||||
{
|
||||
std::shared_ptr<HandlerProvider const> provider_;
|
||||
|
||||
public:
|
||||
HandlerTable(std::shared_ptr<HandlerProvider const> const& provider) : provider_{provider}
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
contains(std::string const& method) const
|
||||
{
|
||||
return provider_->contains(method);
|
||||
}
|
||||
|
||||
std::optional<AnyHandler>
|
||||
getHandler(std::string const& command) const
|
||||
{
|
||||
return provider_->getHandler(command);
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(std::string const& command) const
|
||||
{
|
||||
return provider_->isClioOnly(command);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace RPC
|
||||
@@ -1,105 +0,0 @@
|
||||
#ifndef REPORTING_HANDLERS_H_INCLUDED
|
||||
#define REPORTING_HANDLERS_H_INCLUDED
|
||||
|
||||
#include <rpc/RPC.h>
|
||||
|
||||
namespace RPC {
|
||||
/*
|
||||
* This file just contains declarations for all of the handlers
|
||||
*/
|
||||
|
||||
// account state methods
|
||||
Result
|
||||
doAccountInfo(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountChannels(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountCurrencies(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountLines(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountNFTs(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountObjects(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountOffers(Context const& context);
|
||||
|
||||
Result
|
||||
doGatewayBalances(Context const& context);
|
||||
|
||||
Result
|
||||
doNoRippleCheck(Context const& context);
|
||||
|
||||
// channels methods
|
||||
|
||||
Result
|
||||
doChannelAuthorize(Context const& context);
|
||||
|
||||
Result
|
||||
doChannelVerify(Context const& context);
|
||||
|
||||
// book methods
|
||||
[[nodiscard]] Result
|
||||
doBookChanges(Context const& context);
|
||||
|
||||
Result
|
||||
doBookOffers(Context const& context);
|
||||
|
||||
// NFT methods
|
||||
Result
|
||||
doNFTBuyOffers(Context const& context);
|
||||
|
||||
Result
|
||||
doNFTSellOffers(Context const& context);
|
||||
|
||||
Result
|
||||
doNFTInfo(Context const& context);
|
||||
|
||||
Result
|
||||
doNFTHistory(Context const& context);
|
||||
|
||||
// ledger methods
|
||||
Result
|
||||
doLedger(Context const& context);
|
||||
|
||||
Result
|
||||
doLedgerEntry(Context const& context);
|
||||
|
||||
Result
|
||||
doLedgerData(Context const& context);
|
||||
|
||||
Result
|
||||
doLedgerRange(Context const& context);
|
||||
|
||||
// transaction methods
|
||||
Result
|
||||
doTx(Context const& context);
|
||||
|
||||
Result
|
||||
doTransactionEntry(Context const& context);
|
||||
|
||||
Result
|
||||
doAccountTx(Context const& context);
|
||||
|
||||
// subscriptions
|
||||
Result
|
||||
doSubscribe(Context const& context);
|
||||
|
||||
Result
|
||||
doUnsubscribe(Context const& context);
|
||||
|
||||
// server methods
|
||||
Result
|
||||
doServerInfo(Context const& context);
|
||||
|
||||
// Utility methods
|
||||
Result
|
||||
doRandom(Context const& context);
|
||||
} // namespace RPC
|
||||
#endif
|
||||
29
src/rpc/JS.h
Normal file
29
src/rpc/JS.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ripple/protocol/jss.h>
|
||||
|
||||
// Useful macro for borrowing from ripple::jss
|
||||
// static strings. (J)son (S)trings
|
||||
#define JS(x) ripple::jss::x.c_str()
|
||||
|
||||
// Access (SF)ield name (S)trings
|
||||
#define SFS(x) ripple::x.jsonName.c_str()
|
||||
29
src/rpc/README.md
Normal file
29
src/rpc/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Clio RPC subsystem
|
||||
## Background
|
||||
The RPC subsystem is where the common framework for handling incoming JSON requests is implemented.
|
||||
Currently the NextGen RPC framework is a work in progress and the handlers are not yet implemented using the new common framework classes.
|
||||
|
||||
## Integration plan
|
||||
- Implement base framework - **done**
|
||||
- Migrate handlers one by one, making them injectable, adding unit-tests - **in progress**
|
||||
- Integrate all new handlers into clio in one go
|
||||
- Cover the rest with unit-tests
|
||||
- Release first time with new subsystem active
|
||||
|
||||
## Components
|
||||
See `common` subfolder.
|
||||
|
||||
- **AnyHandler**: The type-erased wrapper that allows for storing different handlers in one map/vector.
|
||||
- **RpcSpec/FieldSpec**: The RPC specification classes, used to specify how incoming JSON is to be validated before it's parsed and passed on to individual handler implementations.
|
||||
- **Validators**: A bunch of supported validators that can be specified as requirements for each **`FieldSpec`** to make up the final **`RpcSpec`** of any given RPC handler.
|
||||
|
||||
## Implementing a (NextGen) handler
|
||||
See `unittests/rpc` for exmaples.
|
||||
|
||||
Handlers need to fulfil the requirements specified by the **`Handler`** concept (see `rpc/common/Concepts.h`):
|
||||
- Expose types:
|
||||
* `Input` - The POD struct which acts as input for the handler
|
||||
* `Output` - The POD struct which acts as output of a valid handler invocation
|
||||
- Have a `spec()` member function returning a const reference to an **`RpcSpec`** describing the JSON input.
|
||||
- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType<Output>`
|
||||
- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions.
|
||||
377
src/rpc/RPC.cpp
377
src/rpc/RPC.cpp
@@ -1,377 +0,0 @@
|
||||
#include <etl/ETLSource.h>
|
||||
#include <rpc/Handlers.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
#include <webserver/HttpBase.h>
|
||||
#include <webserver/WsBase.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
Context::Context(
|
||||
boost::asio::yield_context& yield_,
|
||||
string const& command_,
|
||||
uint32_t version_,
|
||||
boost::json::object const& params_,
|
||||
shared_ptr<BackendInterface const> const& backend_,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions_,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer_,
|
||||
shared_ptr<ReportingETL const> const& etl_,
|
||||
shared_ptr<WsBase> const& session_,
|
||||
util::TagDecoratorFactory const& tagFactory_,
|
||||
Backend::LedgerRange const& range_,
|
||||
Counters& counters_,
|
||||
string const& clientIp_)
|
||||
: Taggable(tagFactory_)
|
||||
, yield(yield_)
|
||||
, method(command_)
|
||||
, version(version_)
|
||||
, params(params_)
|
||||
, backend(backend_)
|
||||
, subscriptions(subscriptions_)
|
||||
, balancer(balancer_)
|
||||
, etl(etl_)
|
||||
, session(session_)
|
||||
, range(range_)
|
||||
, counters(counters_)
|
||||
, clientIp(clientIp_)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << tag() << "new Context created";
|
||||
}
|
||||
|
||||
optional<Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
shared_ptr<BackendInterface const> const& backend,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
shared_ptr<ReportingETL const> const& etl,
|
||||
shared_ptr<WsBase> const& session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
Counters& counters,
|
||||
string const& clientIp)
|
||||
{
|
||||
boost::json::value commandValue = nullptr;
|
||||
if (!request.contains("command") && request.contains("method"))
|
||||
commandValue = request.at("method");
|
||||
else if (request.contains("command") && !request.contains("method"))
|
||||
commandValue = request.at("command");
|
||||
|
||||
if (!commandValue.is_string())
|
||||
return {};
|
||||
|
||||
string command = commandValue.as_string().c_str();
|
||||
|
||||
return make_optional<Context>(
|
||||
yc,
|
||||
command,
|
||||
1,
|
||||
request,
|
||||
backend,
|
||||
subscriptions,
|
||||
balancer,
|
||||
etl,
|
||||
session,
|
||||
tagFactory,
|
||||
range,
|
||||
counters,
|
||||
clientIp);
|
||||
}
|
||||
|
||||
optional<Context>
|
||||
make_HttpContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
shared_ptr<BackendInterface const> const& backend,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
shared_ptr<ReportingETL const> const& etl,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
RPC::Counters& counters,
|
||||
string const& clientIp)
|
||||
{
|
||||
if (!request.contains("method") || !request.at("method").is_string())
|
||||
return {};
|
||||
|
||||
string const& command = request.at("method").as_string().c_str();
|
||||
|
||||
if (command == "subscribe" || command == "unsubscribe")
|
||||
return {};
|
||||
|
||||
if (!request.at("params").is_array())
|
||||
return {};
|
||||
|
||||
boost::json::array const& array = request.at("params").as_array();
|
||||
|
||||
if (array.size() != 1)
|
||||
return {};
|
||||
|
||||
if (!array.at(0).is_object())
|
||||
return {};
|
||||
|
||||
return make_optional<Context>(
|
||||
yc,
|
||||
command,
|
||||
1,
|
||||
array.at(0).as_object(),
|
||||
backend,
|
||||
subscriptions,
|
||||
balancer,
|
||||
etl,
|
||||
nullptr,
|
||||
tagFactory,
|
||||
range,
|
||||
counters,
|
||||
clientIp);
|
||||
}
|
||||
|
||||
using LimitRange = tuple<uint32_t, uint32_t, uint32_t>;
|
||||
using HandlerFunction = function<Result(Context const&)>;
|
||||
|
||||
struct Handler
|
||||
{
|
||||
string method;
|
||||
function<Result(Context const&)> handler;
|
||||
optional<LimitRange> limit;
|
||||
bool isClioOnly = false;
|
||||
};
|
||||
|
||||
class HandlerTable
|
||||
{
|
||||
unordered_map<string, Handler> handlerMap_;
|
||||
|
||||
public:
|
||||
HandlerTable(initializer_list<Handler> handlers)
|
||||
{
|
||||
for (auto const& handler : handlers)
|
||||
{
|
||||
handlerMap_[handler.method] = move(handler);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
contains(string const& method)
|
||||
{
|
||||
return handlerMap_.contains(method);
|
||||
}
|
||||
|
||||
optional<LimitRange>
|
||||
getLimitRange(string const& command)
|
||||
{
|
||||
if (!handlerMap_.contains(command))
|
||||
return {};
|
||||
|
||||
return handlerMap_[command].limit;
|
||||
}
|
||||
|
||||
optional<HandlerFunction>
|
||||
getHandler(string const& command)
|
||||
{
|
||||
if (!handlerMap_.contains(command))
|
||||
return {};
|
||||
|
||||
return handlerMap_[command].handler;
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(string const& command)
|
||||
{
|
||||
return handlerMap_.contains(command) && handlerMap_[command].isClioOnly;
|
||||
}
|
||||
};
|
||||
|
||||
static HandlerTable handlerTable{
|
||||
{"account_channels", &doAccountChannels, LimitRange{10, 50, 256}},
|
||||
{"account_currencies", &doAccountCurrencies, {}},
|
||||
{"account_info", &doAccountInfo, {}},
|
||||
{"account_lines", &doAccountLines, LimitRange{10, 50, 256}},
|
||||
{"account_nfts", &doAccountNFTs, LimitRange{1, 5, 10}},
|
||||
{"account_objects", &doAccountObjects, LimitRange{10, 50, 256}},
|
||||
{"account_offers", &doAccountOffers, LimitRange{10, 50, 256}},
|
||||
{"account_tx", &doAccountTx, LimitRange{1, 50, 100}},
|
||||
{"gateway_balances", &doGatewayBalances, {}},
|
||||
{"noripple_check", &doNoRippleCheck, LimitRange{1, 300, 500}},
|
||||
{"book_changes", &doBookChanges, {}},
|
||||
{"book_offers", &doBookOffers, LimitRange{1, 50, 100}},
|
||||
{"ledger", &doLedger, {}},
|
||||
{"ledger_data", &doLedgerData, LimitRange{1, 100, 2048}},
|
||||
{"nft_buy_offers", &doNFTBuyOffers, LimitRange{1, 50, 100}},
|
||||
{"nft_history", &doNFTHistory, LimitRange{1, 50, 100}, true},
|
||||
{"nft_info", &doNFTInfo, {}, true},
|
||||
{"nft_sell_offers", &doNFTSellOffers, LimitRange{1, 50, 100}},
|
||||
{"ledger_entry", &doLedgerEntry, {}},
|
||||
{"ledger_range", &doLedgerRange, {}},
|
||||
{"subscribe", &doSubscribe, {}},
|
||||
{"server_info", &doServerInfo, {}},
|
||||
{"unsubscribe", &doUnsubscribe, {}},
|
||||
{"tx", &doTx, {}},
|
||||
{"transaction_entry", &doTransactionEntry, {}},
|
||||
{"random", &doRandom, {}}};
|
||||
|
||||
static unordered_set<string> forwardCommands{
|
||||
"submit",
|
||||
"submit_multisigned",
|
||||
"fee",
|
||||
"ledger_closed",
|
||||
"ledger_current",
|
||||
"ripple_path_find",
|
||||
"manifest",
|
||||
"channel_authorize",
|
||||
"channel_verify"};
|
||||
|
||||
bool
|
||||
validHandler(string const& method)
|
||||
{
|
||||
return handlerTable.contains(method) || forwardCommands.contains(method);
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(string const& method)
|
||||
{
|
||||
return handlerTable.isClioOnly(method);
|
||||
}
|
||||
|
||||
bool
|
||||
shouldSuppressValidatedFlag(RPC::Context const& context)
|
||||
{
|
||||
return boost::iequals(context.method, "subscribe") ||
|
||||
boost::iequals(context.method, "unsubscribe");
|
||||
}
|
||||
|
||||
Status
|
||||
getLimit(RPC::Context const& context, uint32_t& limit)
|
||||
{
|
||||
if (!handlerTable.getHandler(context.method))
|
||||
return Status{RippledError::rpcUNKNOWN_COMMAND};
|
||||
|
||||
if (!handlerTable.getLimitRange(context.method))
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "rpcDoesNotRequireLimit"};
|
||||
|
||||
auto [lo, def, hi] = *handlerTable.getLimitRange(context.method);
|
||||
|
||||
if (context.params.contains(JS(limit)))
|
||||
{
|
||||
string errMsg = "Invalid field 'limit', not unsigned integer.";
|
||||
if (!context.params.at(JS(limit)).is_int64())
|
||||
return Status{RippledError::rpcINVALID_PARAMS, errMsg};
|
||||
|
||||
int input = context.params.at(JS(limit)).as_int64();
|
||||
if (input <= 0)
|
||||
return Status{RippledError::rpcINVALID_PARAMS, errMsg};
|
||||
|
||||
limit = clamp(static_cast<uint32_t>(input), lo, hi);
|
||||
}
|
||||
else
|
||||
{
|
||||
limit = def;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool
|
||||
shouldForwardToRippled(Context const& ctx)
|
||||
{
|
||||
auto request = ctx.params;
|
||||
|
||||
if (isClioOnly(ctx.method))
|
||||
return false;
|
||||
|
||||
if (forwardCommands.find(ctx.method) != forwardCommands.end())
|
||||
return true;
|
||||
|
||||
if (specifiesCurrentOrClosedLedger(request))
|
||||
return true;
|
||||
|
||||
if (ctx.method == "account_info" && request.contains("queue") &&
|
||||
request.at("queue").as_bool())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Result
|
||||
buildResponse(Context const& ctx)
|
||||
{
|
||||
if (shouldForwardToRippled(ctx))
|
||||
{
|
||||
boost::json::object toForward = ctx.params;
|
||||
toForward["command"] = ctx.method;
|
||||
|
||||
auto res =
|
||||
ctx.balancer->forwardToRippled(toForward, ctx.clientIp, ctx.yield);
|
||||
|
||||
ctx.counters.rpcForwarded(ctx.method);
|
||||
|
||||
if (!res)
|
||||
return Status{RippledError::rpcFAILED_TO_FORWARD};
|
||||
|
||||
if (res->contains("result") && res->at("result").is_object())
|
||||
return res->at("result").as_object();
|
||||
|
||||
return *res;
|
||||
}
|
||||
|
||||
if (ctx.method == "ping")
|
||||
return boost::json::object{};
|
||||
|
||||
if (ctx.backend->isTooBusy())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< __func__ << " Database is too busy. Rejecting request";
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
|
||||
auto method = handlerTable.getHandler(ctx.method);
|
||||
|
||||
if (!method)
|
||||
return Status{RippledError::rpcUNKNOWN_COMMAND};
|
||||
|
||||
try
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< ctx.tag() << __func__ << " start executing rpc `" << ctx.method
|
||||
<< '`';
|
||||
auto v = (*method)(ctx);
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< ctx.tag() << __func__ << " finish executing rpc `" << ctx.method
|
||||
<< '`';
|
||||
|
||||
if (auto object = get_if<boost::json::object>(&v);
|
||||
object && not shouldSuppressValidatedFlag(ctx))
|
||||
{
|
||||
(*object)["validated"] = true;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
catch (InvalidParamsError const& err)
|
||||
{
|
||||
return Status{RippledError::rpcINVALID_PARAMS, err.what()};
|
||||
}
|
||||
catch (AccountNotFoundError const& err)
|
||||
{
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, err.what()};
|
||||
}
|
||||
catch (Backend::DatabaseTimeout const& t)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout";
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
catch (exception const& err)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< ctx.tag() << __func__ << " caught exception : " << err.what();
|
||||
return Status{RippledError::rpcINTERNAL};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
148
src/rpc/RPC.h
148
src/rpc/RPC.h
@@ -1,148 +0,0 @@
|
||||
#ifndef REPORTING_RPC_H_INCLUDED
|
||||
#define REPORTING_RPC_H_INCLUDED
|
||||
|
||||
#include <rpc/Errors.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <rpc/Counters.h>
|
||||
#include <util/Taggable.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
/*
|
||||
* This file contains various classes necessary for executing RPC handlers.
|
||||
* Context gives the handlers access to various other parts of the application
|
||||
* Status is used to report errors.
|
||||
* And lastly, there are various functions for making Contexts, Statuses and
|
||||
* serializing Status to JSON.
|
||||
* This file is meant to contain any class or function that code outside of the
|
||||
* rpc folder needs to use. For helper functions or classes used within the rpc
|
||||
* folder, use RPCHelpers.h.
|
||||
*/
|
||||
|
||||
class WsBase;
|
||||
class SubscriptionManager;
|
||||
class ETLLoadBalancer;
|
||||
class ReportingETL;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
struct Context : public util::Taggable
|
||||
{
|
||||
boost::asio::yield_context& yield;
|
||||
std::string method;
|
||||
std::uint32_t version;
|
||||
boost::json::object const& params;
|
||||
std::shared_ptr<BackendInterface const> const& backend;
|
||||
// this needs to be an actual shared_ptr, not a reference. The above
|
||||
// references refer to shared_ptr members of WsBase, but WsBase contains
|
||||
// SubscriptionManager as a weak_ptr, to prevent a shared_ptr cycle.
|
||||
std::shared_ptr<SubscriptionManager> subscriptions;
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer;
|
||||
std::shared_ptr<ReportingETL const> const& etl;
|
||||
std::shared_ptr<WsBase> session;
|
||||
Backend::LedgerRange const& range;
|
||||
Counters& counters;
|
||||
std::string clientIp;
|
||||
|
||||
Context(
|
||||
boost::asio::yield_context& yield_,
|
||||
std::string const& command_,
|
||||
std::uint32_t version_,
|
||||
boost::json::object const& params_,
|
||||
std::shared_ptr<BackendInterface const> const& backend_,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions_,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer_,
|
||||
std::shared_ptr<ReportingETL const> const& etl_,
|
||||
std::shared_ptr<WsBase> const& session_,
|
||||
util::TagDecoratorFactory const& tagFactory_,
|
||||
Backend::LedgerRange const& range_,
|
||||
Counters& counters_,
|
||||
std::string const& clientIp_);
|
||||
};
|
||||
|
||||
struct AccountCursor
|
||||
{
|
||||
ripple::uint256 index;
|
||||
std::uint32_t hint;
|
||||
|
||||
std::string
|
||||
toString()
|
||||
{
|
||||
return ripple::strHex(index) + "," + std::to_string(hint);
|
||||
}
|
||||
|
||||
bool
|
||||
isNonZero()
|
||||
{
|
||||
return index.isNonZero() || hint != 0;
|
||||
}
|
||||
};
|
||||
|
||||
using Result = std::variant<Status, boost::json::object>;
|
||||
|
||||
std::optional<Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL const> const& etl,
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
Counters& counters,
|
||||
std::string const& clientIp);
|
||||
|
||||
std::optional<Context>
|
||||
make_HttpContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL const> const& etl,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
Counters& counters,
|
||||
std::string const& clientIp);
|
||||
|
||||
Result
|
||||
buildResponse(Context const& ctx);
|
||||
|
||||
bool
|
||||
validHandler(std::string const& method);
|
||||
|
||||
bool
|
||||
isClioOnly(std::string const& method);
|
||||
|
||||
Status
|
||||
getLimit(RPC::Context const& context, std::uint32_t& limit);
|
||||
|
||||
template <class T>
|
||||
void
|
||||
logDuration(Context const& ctx, T const& dur)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << ctx.tag() << "Request processing duration = "
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(dur).count()
|
||||
<< " milliseconds. request = " << ctx.params;
|
||||
auto seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(dur).count();
|
||||
if (seconds > 10)
|
||||
BOOST_LOG_TRIVIAL(error) << ss.str();
|
||||
else if (seconds > 1)
|
||||
BOOST_LOG_TRIVIAL(warning) << ss.str();
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(info) << ss.str();
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
#endif // REPORTING_RPC_H_INCLUDED
|
||||
275
src/rpc/RPCEngine.h
Normal file
275
src/rpc/RPCEngine.h
Normal file
@@ -0,0 +1,275 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <config/Config.h>
|
||||
#include <etl/ETLSource.h>
|
||||
#include <log/Logger.h>
|
||||
#include <rpc/Counters.h>
|
||||
#include <rpc/Errors.h>
|
||||
#include <rpc/HandlerTable.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
#include <rpc/common/AnyHandler.h>
|
||||
#include <rpc/common/Types.h>
|
||||
#include <rpc/common/impl/AdminVerificationStrategy.h>
|
||||
#include <util/Taggable.h>
|
||||
#include <webserver/Context.h>
|
||||
#include <webserver/DOSGuard.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
|
||||
class WsBase;
|
||||
class SubscriptionManager;
|
||||
class ETLLoadBalancer;
|
||||
class ReportingETL;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief The RPC engine that ties all RPC-related functionality together
|
||||
*/
|
||||
template <typename AdminVerificationStrategyType>
|
||||
class RPCEngineBase
|
||||
{
|
||||
clio::Logger perfLog_{"Performance"};
|
||||
clio::Logger log_{"RPC"};
|
||||
|
||||
std::shared_ptr<BackendInterface> backend_;
|
||||
std::shared_ptr<SubscriptionManager> subscriptions_;
|
||||
std::shared_ptr<ETLLoadBalancer> balancer_;
|
||||
std::reference_wrapper<clio::DOSGuard const> dosGuard_;
|
||||
std::reference_wrapper<WorkQueue> workQueue_;
|
||||
std::reference_wrapper<Counters> counters_;
|
||||
|
||||
HandlerTable handlerTable_;
|
||||
AdminVerificationStrategyType adminVerifier_;
|
||||
|
||||
public:
|
||||
RPCEngineBase(
|
||||
std::shared_ptr<BackendInterface> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL> const& etl,
|
||||
clio::DOSGuard const& dosGuard,
|
||||
WorkQueue& workQueue,
|
||||
Counters& counters,
|
||||
std::shared_ptr<HandlerProvider const> const& handlerProvider)
|
||||
: backend_{backend}
|
||||
, subscriptions_{subscriptions}
|
||||
, balancer_{balancer}
|
||||
, dosGuard_{std::cref(dosGuard)}
|
||||
, workQueue_{std::ref(workQueue)}
|
||||
, counters_{std::ref(counters)}
|
||||
, handlerTable_{handlerProvider}
|
||||
{
|
||||
}
|
||||
|
||||
static std::shared_ptr<RPCEngineBase>
|
||||
make_RPCEngine(
|
||||
clio::Config const& config,
|
||||
std::shared_ptr<BackendInterface> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL> const& etl,
|
||||
clio::DOSGuard const& dosGuard,
|
||||
WorkQueue& workQueue,
|
||||
Counters& counters,
|
||||
std::shared_ptr<HandlerProvider const> const& handlerProvider)
|
||||
{
|
||||
return std::make_shared<RPCEngineBase>(
|
||||
backend, subscriptions, balancer, etl, dosGuard, workQueue, counters, handlerProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main request processor routine
|
||||
* @param ctx The @ref Context of the request
|
||||
*/
|
||||
Result
|
||||
buildResponse(Web::Context const& ctx)
|
||||
{
|
||||
if (shouldForwardToRippled(ctx))
|
||||
{
|
||||
auto toForward = ctx.params;
|
||||
toForward["command"] = ctx.method;
|
||||
|
||||
auto const res = balancer_->forwardToRippled(toForward, ctx.clientIp, ctx.yield);
|
||||
notifyForwarded(ctx.method);
|
||||
|
||||
if (!res)
|
||||
return Status{RippledError::rpcFAILED_TO_FORWARD};
|
||||
|
||||
return *res;
|
||||
}
|
||||
|
||||
if (backend_->isTooBusy())
|
||||
{
|
||||
log_.error() << "Database is too busy. Rejecting request";
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
|
||||
auto const method = handlerTable_.getHandler(ctx.method);
|
||||
if (!method)
|
||||
return Status{RippledError::rpcUNKNOWN_COMMAND};
|
||||
|
||||
try
|
||||
{
|
||||
perfLog_.debug() << ctx.tag() << " start executing rpc `" << ctx.method << '`';
|
||||
|
||||
auto const isAdmin = adminVerifier_.isAdmin(ctx.clientIp);
|
||||
auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp};
|
||||
auto const v = (*method).process(ctx.params, context);
|
||||
|
||||
perfLog_.debug() << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
|
||||
|
||||
if (v)
|
||||
return v->as_object();
|
||||
else
|
||||
return Status{v.error()};
|
||||
}
|
||||
catch (InvalidParamsError const& err)
|
||||
{
|
||||
return Status{RippledError::rpcINVALID_PARAMS, err.what()};
|
||||
}
|
||||
catch (AccountNotFoundError const& err)
|
||||
{
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, err.what()};
|
||||
}
|
||||
catch (Backend::DatabaseTimeout const& t)
|
||||
{
|
||||
log_.error() << "Database timeout";
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
catch (std::exception const& err)
|
||||
{
|
||||
log_.error() << ctx.tag() << " caught exception: " << err.what();
|
||||
return Status{RippledError::rpcINTERNAL};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Used to schedule request processing onto the work queue
|
||||
* @param func The lambda to execute when this request is handled
|
||||
* @param ip The ip address for which this request is being executed
|
||||
*/
|
||||
template <typename Fn>
|
||||
bool
|
||||
post(Fn&& func, std::string const& ip)
|
||||
{
|
||||
return workQueue_.get().postCoro(std::forward<Fn>(func), dosGuard_.get().isWhiteListed(ip));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Notify the system that specified method was executed
|
||||
* @param method
|
||||
* @param duration The time it took to execute the method specified in
|
||||
* microseconds
|
||||
*/
|
||||
void
|
||||
notifyComplete(std::string const& method, std::chrono::microseconds const& duration)
|
||||
{
|
||||
if (validHandler(method))
|
||||
counters_.get().rpcComplete(method, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Notify the system that specified method failed to execute
|
||||
* @param method
|
||||
*/
|
||||
void
|
||||
notifyErrored(std::string const& method)
|
||||
{
|
||||
if (validHandler(method))
|
||||
counters_.get().rpcErrored(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Notify the system that specified method execution was forwarded to rippled
|
||||
* @param method
|
||||
*/
|
||||
void
|
||||
notifyForwarded(std::string const& method)
|
||||
{
|
||||
if (validHandler(method))
|
||||
counters_.get().rpcForwarded(method);
|
||||
}
|
||||
|
||||
private:
|
||||
bool
|
||||
shouldForwardToRippled(Web::Context const& ctx) const
|
||||
{
|
||||
auto request = ctx.params;
|
||||
|
||||
if (isClioOnly(ctx.method))
|
||||
return false;
|
||||
|
||||
if (isForwardCommand(ctx.method))
|
||||
return true;
|
||||
|
||||
if (specifiesCurrentOrClosedLedger(request))
|
||||
return true;
|
||||
|
||||
if (ctx.method == "account_info" && request.contains("queue") && request.at("queue").as_bool())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
isForwardCommand(std::string const& method) const
|
||||
{
|
||||
static std::unordered_set<std::string> const FORWARD_COMMANDS{
|
||||
"submit",
|
||||
"submit_multisigned",
|
||||
"fee",
|
||||
"ledger_closed",
|
||||
"ledger_current",
|
||||
"ripple_path_find",
|
||||
"manifest",
|
||||
"channel_authorize",
|
||||
"channel_verify",
|
||||
};
|
||||
|
||||
return FORWARD_COMMANDS.contains(method);
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(std::string const& method) const
|
||||
{
|
||||
return handlerTable_.isClioOnly(method);
|
||||
}
|
||||
|
||||
bool
|
||||
validHandler(std::string const& method) const
|
||||
{
|
||||
return handlerTable_.contains(method) || isForwardCommand(method);
|
||||
}
|
||||
};
|
||||
|
||||
using RPCEngine = RPCEngineBase<detail::IPAdminVerificationStrategy>;
|
||||
|
||||
} // namespace RPC
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,41 @@
|
||||
|
||||
#ifndef XRPL_REPORTING_RPCHELPERS_H_INCLUDED
|
||||
#define XRPL_REPORTING_RPCHELPERS_H_INCLUDED
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
* This file contains a variety of utility functions used when executing
|
||||
* the handlers
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* This file contains a variety of utility functions used when executing the handlers.
|
||||
*/
|
||||
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/STLedgerEntry.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <backend/BackendInterface.h>
|
||||
#include <rpc/RPC.h>
|
||||
#include <rpc/JS.h>
|
||||
#include <rpc/common/Types.h>
|
||||
#include <webserver/Context.h>
|
||||
|
||||
// Useful macro for borrowing from ripple::jss
|
||||
// static strings. (J)son (S)trings
|
||||
#define JS(x) ripple::jss::x.c_str()
|
||||
|
||||
// Access (SF)ield name (S)trings
|
||||
#define SFS(x) ripple::x.jsonName.c_str()
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
std::optional<ripple::AccountID>
|
||||
accountFromStringStrict(std::string const& account);
|
||||
|
||||
@@ -32,26 +46,15 @@ std::uint64_t
|
||||
getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID);
|
||||
|
||||
std::optional<AccountCursor>
|
||||
parseAccountCursor(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t seq,
|
||||
std::optional<std::string> jsonCursor,
|
||||
ripple::AccountID const& accountID,
|
||||
boost::asio::yield_context& yield);
|
||||
parseAccountCursor(std::optional<std::string> jsonCursor);
|
||||
|
||||
// TODO this function should probably be in a different file and namespace
|
||||
std::pair<
|
||||
std::shared_ptr<ripple::STTx const>,
|
||||
std::shared_ptr<ripple::STObject const>>
|
||||
std::pair<std::shared_ptr<ripple::STTx const>, std::shared_ptr<ripple::STObject const>>
|
||||
deserializeTxPlusMeta(Backend::TransactionAndMetadata const& blobs);
|
||||
|
||||
// TODO this function should probably be in a different file and namespace
|
||||
std::pair<
|
||||
std::shared_ptr<ripple::STTx const>,
|
||||
std::shared_ptr<ripple::TxMeta const>>
|
||||
deserializeTxPlusMeta(
|
||||
Backend::TransactionAndMetadata const& blobs,
|
||||
std::uint32_t seq);
|
||||
std::pair<std::shared_ptr<ripple::STTx const>, std::shared_ptr<ripple::TxMeta const>>
|
||||
deserializeTxPlusMeta(Backend::TransactionAndMetadata const& blobs, std::uint32_t seq);
|
||||
|
||||
std::pair<boost::json::object, boost::json::object>
|
||||
toExpandedJson(Backend::TransactionAndMetadata const& blobs);
|
||||
@@ -87,7 +90,15 @@ generatePubLedgerMessage(
|
||||
std::uint32_t txnCount);
|
||||
|
||||
std::variant<Status, ripple::LedgerInfo>
|
||||
ledgerInfoFromRequest(Context const& ctx);
|
||||
ledgerInfoFromRequest(std::shared_ptr<Backend::BackendInterface const> const& backend, Web::Context const& ctx);
|
||||
|
||||
std::variant<Status, ripple::LedgerInfo>
|
||||
getLedgerInfoFromHashOrSeq(
|
||||
BackendInterface const& backend,
|
||||
boost::asio::yield_context& yield,
|
||||
std::optional<std::string> ledgerHash,
|
||||
std::optional<uint32_t> ledgerIndex,
|
||||
uint32_t maxSeq);
|
||||
|
||||
std::variant<Status, AccountCursor>
|
||||
traverseOwnedNodes(
|
||||
@@ -111,11 +122,24 @@ traverseOwnedNodes(
|
||||
boost::asio::yield_context& yield,
|
||||
std::function<void(ripple::SLE&&)> atOwnedNode);
|
||||
|
||||
// Remove the account check from traverseOwnedNodes
|
||||
// Account check has been done by framework,remove it from internal function
|
||||
std::variant<Status, AccountCursor>
|
||||
ngTraverseOwnedNodes(
|
||||
BackendInterface const& backend,
|
||||
ripple::AccountID const& accountID,
|
||||
std::uint32_t sequence,
|
||||
std::uint32_t limit,
|
||||
std::optional<std::string> jsonCursor,
|
||||
boost::asio::yield_context& yield,
|
||||
std::function<void(ripple::SLE&&)> atOwnedNode);
|
||||
|
||||
std::shared_ptr<ripple::SLE const>
|
||||
read(
|
||||
std::shared_ptr<Backend::BackendInterface const> const& backend,
|
||||
ripple::Keylet const& keylet,
|
||||
ripple::LedgerInfo const& lgrInfo,
|
||||
Context const& context);
|
||||
Web::Context const& context);
|
||||
|
||||
std::variant<Status, std::pair<ripple::PublicKey, ripple::SecretKey>>
|
||||
keypairFromRequst(boost::json::object const& request);
|
||||
@@ -183,6 +207,9 @@ postProcessOrderBook(
|
||||
std::uint32_t ledgerSequence,
|
||||
boost::asio::yield_context& yield);
|
||||
|
||||
std::variant<Status, ripple::Book>
|
||||
parseBook(ripple::Currency pays, ripple::AccountID payIssuer, ripple::Currency gets, ripple::AccountID getIssuer);
|
||||
|
||||
std::variant<Status, ripple::Book>
|
||||
parseBook(boost::json::object const& request);
|
||||
|
||||
@@ -193,10 +220,7 @@ std::optional<std::uint32_t>
|
||||
getUInt(boost::json::object const& request, std::string const& field);
|
||||
|
||||
std::uint32_t
|
||||
getUInt(
|
||||
boost::json::object const& request,
|
||||
std::string const& field,
|
||||
std::uint32_t dfault);
|
||||
getUInt(boost::json::object const& request, std::string const& field, std::uint32_t dfault);
|
||||
|
||||
std::uint32_t
|
||||
getRequiredUInt(boost::json::object const& request, std::string const& field);
|
||||
@@ -205,10 +229,7 @@ std::optional<bool>
|
||||
getBool(boost::json::object const& request, std::string const& field);
|
||||
|
||||
bool
|
||||
getBool(
|
||||
boost::json::object const& request,
|
||||
std::string const& field,
|
||||
bool dfault);
|
||||
getBool(boost::json::object const& request, std::string const& field, bool dfault);
|
||||
|
||||
bool
|
||||
getRequiredBool(boost::json::object const& request, std::string const& field);
|
||||
@@ -220,10 +241,7 @@ std::string
|
||||
getRequiredString(boost::json::object const& request, std::string const& field);
|
||||
|
||||
std::string
|
||||
getString(
|
||||
boost::json::object const& request,
|
||||
std::string const& field,
|
||||
std::string dfault);
|
||||
getString(boost::json::object const& request, std::string const& field, std::string dfault);
|
||||
|
||||
Status
|
||||
getHexMarker(boost::json::object const& request, ripple::uint256& marker);
|
||||
@@ -232,10 +250,7 @@ Status
|
||||
getAccount(boost::json::object const& request, ripple::AccountID& accountId);
|
||||
|
||||
Status
|
||||
getAccount(
|
||||
boost::json::object const& request,
|
||||
ripple::AccountID& destAccount,
|
||||
boost::string_view const& field);
|
||||
getAccount(boost::json::object const& request, ripple::AccountID& destAccount, boost::string_view const& field);
|
||||
|
||||
Status
|
||||
getOptionalAccount(
|
||||
@@ -255,22 +270,24 @@ specifiesCurrentOrClosedLedger(boost::json::object const& request);
|
||||
std::variant<ripple::uint256, Status>
|
||||
getNFTID(boost::json::object const& request);
|
||||
|
||||
// This function is the driver for both `account_tx` and `nft_tx` and should
|
||||
// be used for any future transaction enumeration APIs.
|
||||
std::variant<Status, boost::json::object>
|
||||
traverseTransactions(
|
||||
Context const& context,
|
||||
std::function<Backend::TransactionsAndCursor(
|
||||
std::shared_ptr<Backend::BackendInterface const> const& backend,
|
||||
std::uint32_t const,
|
||||
bool const,
|
||||
std::optional<Backend::TransactionsCursor> const&,
|
||||
boost::asio::yield_context& yield)> transactionFetcher);
|
||||
template <class T>
|
||||
void
|
||||
logDuration(Web::Context const& ctx, T const& dur)
|
||||
{
|
||||
using boost::json::serialize;
|
||||
|
||||
[[nodiscard]] boost::json::object const
|
||||
computeBookChanges(
|
||||
ripple::LedgerInfo const& lgrInfo,
|
||||
std::vector<Backend::TransactionAndMetadata> const& transactions);
|
||||
static clio::Logger log{"RPC"};
|
||||
auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(dur).count();
|
||||
auto const seconds = std::chrono::duration_cast<std::chrono::seconds>(dur).count();
|
||||
auto const msg =
|
||||
fmt::format("Request processing duration = {} milliseconds. request = {}", millis, serialize(ctx.params));
|
||||
|
||||
if (seconds > 10)
|
||||
log.error() << ctx.tag() << msg;
|
||||
else if (seconds > 1)
|
||||
log.warn() << ctx.tag() << msg;
|
||||
else
|
||||
log.info() << ctx.tag() << msg;
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
#endif
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <rpc/WorkQueue.h>
|
||||
|
||||
WorkQueue::WorkQueue(std::uint32_t numWorkers, uint32_t maxSize)
|
||||
{
|
||||
if (maxSize != 0)
|
||||
maxSize_ = maxSize;
|
||||
|
||||
while (--numWorkers)
|
||||
{
|
||||
threads_.emplace_back([this] { ioc_.run(); });
|
||||
}
|
||||
}
|
||||
|
||||
WorkQueue::~WorkQueue()
|
||||
{
|
||||
work_.reset();
|
||||
for (auto& thread : threads_)
|
||||
thread.join();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
#ifndef CLIO_WORK_QUEUE_H
|
||||
#define CLIO_WORK_QUEUE_H
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <config/Config.h>
|
||||
#include <log/Logger.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -20,9 +40,27 @@ class WorkQueue
|
||||
|
||||
std::atomic_uint64_t curSize_ = 0;
|
||||
uint32_t maxSize_ = std::numeric_limits<uint32_t>::max();
|
||||
clio::Logger log_{"RPC"};
|
||||
|
||||
std::vector<std::thread> threads_ = {};
|
||||
boost::asio::io_context ioc_ = {};
|
||||
std::optional<boost::asio::io_context::work> work_{ioc_};
|
||||
|
||||
public:
|
||||
WorkQueue(std::uint32_t numWorkers, uint32_t maxSize = 0);
|
||||
~WorkQueue();
|
||||
|
||||
static WorkQueue
|
||||
make_WorkQueue(clio::Config const& config)
|
||||
{
|
||||
static clio::Logger log{"RPC"};
|
||||
auto const serverConfig = config.section("server");
|
||||
auto const numThreads = config.valueOr<uint32_t>("workers", std::thread::hardware_concurrency());
|
||||
auto const maxQueueSize = serverConfig.valueOr<uint32_t>("max_queue_size", 0); // 0 is no limit
|
||||
|
||||
log.info() << "Number of workers = " << numThreads << ". Max queue size = " << maxQueueSize;
|
||||
return WorkQueue{numThreads, maxQueueSize};
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
bool
|
||||
@@ -30,52 +68,42 @@ public:
|
||||
{
|
||||
if (curSize_ >= maxSize_ && !isWhiteListed)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< __func__
|
||||
<< " queue is full. rejecting job. current size = " << curSize_
|
||||
<< " max size = " << maxSize_;
|
||||
log_.warn() << "Queue is full. rejecting job. current size = " << curSize_ << " max size = " << maxSize_;
|
||||
return false;
|
||||
}
|
||||
|
||||
++curSize_;
|
||||
auto start = std::chrono::system_clock::now();
|
||||
// Each time we enqueue a job, we want to post a symmetrical job that
|
||||
// will dequeue and run the job at the front of the job queue.
|
||||
boost::asio::spawn(
|
||||
ioc_,
|
||||
[this, f = std::move(f), start](boost::asio::yield_context yield) {
|
||||
auto run = std::chrono::system_clock::now();
|
||||
auto wait =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
run - start)
|
||||
.count();
|
||||
// increment queued_ here, in the same place we implement
|
||||
// durationUs_
|
||||
++queued_;
|
||||
durationUs_ += wait;
|
||||
BOOST_LOG_TRIVIAL(debug) << "WorkQueue wait time = " << wait
|
||||
<< " queue size = " << curSize_;
|
||||
f(yield);
|
||||
--curSize_;
|
||||
});
|
||||
|
||||
// Each time we enqueue a job, we want to post a symmetrical job that will dequeue and run the job at the front
|
||||
// of the job queue.
|
||||
boost::asio::spawn(ioc_, [this, f = std::move(f), start](auto yield) {
|
||||
auto const run = std::chrono::system_clock::now();
|
||||
auto const wait = std::chrono::duration_cast<std::chrono::microseconds>(run - start).count();
|
||||
|
||||
// increment queued_ here, in the same place we implement durationUs_
|
||||
++queued_;
|
||||
durationUs_ += wait;
|
||||
log_.info() << "WorkQueue wait time = " << wait << " queue size = " << curSize_;
|
||||
|
||||
f(yield);
|
||||
|
||||
--curSize_;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
report() const
|
||||
{
|
||||
boost::json::object obj;
|
||||
auto obj = boost::json::object{};
|
||||
|
||||
obj["queued"] = queued_;
|
||||
obj["queued_duration_us"] = durationUs_;
|
||||
obj["current_queue_size"] = curSize_;
|
||||
obj["max_queue_size"] = maxSize_;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::thread> threads_ = {};
|
||||
|
||||
boost::asio::io_context ioc_ = {};
|
||||
std::optional<boost::asio::io_context::work> work_{ioc_};
|
||||
};
|
||||
|
||||
#endif // CLIO_WORK_QUEUE_H
|
||||
|
||||
140
src/rpc/common/AnyHandler.h
Normal file
140
src/rpc/common/AnyHandler.h
Normal file
@@ -0,0 +1,140 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/Concepts.h>
|
||||
#include <rpc/common/Types.h>
|
||||
#include <rpc/common/impl/Processors.h>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief A type-erased Handler that can contain any (NextGen) RPC handler class
|
||||
*
|
||||
* This allows to store different handlers in one map/vector etc.
|
||||
* Support for copying was added in order to allow storing in a
|
||||
* map/unordered_map using the initializer_list constructor.
|
||||
*/
|
||||
class AnyHandler final
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Type-erases any handler class.
|
||||
*
|
||||
* @tparam HandlerType The real type of wrapped handler class
|
||||
* @tparam ProcessingStrategy A strategy that implements how processing of JSON is to be done
|
||||
* @param handler The handler to wrap. Required to fulfil the @ref Handler concept.
|
||||
*/
|
||||
template <Handler HandlerType, typename ProcessingStrategy = detail::DefaultProcessor<HandlerType>>
|
||||
/* implicit */ AnyHandler(HandlerType&& handler)
|
||||
: pimpl_{std::make_unique<Model<HandlerType, ProcessingStrategy>>(std::forward<HandlerType>(handler))}
|
||||
{
|
||||
}
|
||||
|
||||
~AnyHandler() = default;
|
||||
AnyHandler(AnyHandler const& other) : pimpl_{other.pimpl_->clone()}
|
||||
{
|
||||
}
|
||||
|
||||
AnyHandler&
|
||||
operator=(AnyHandler const& rhs)
|
||||
{
|
||||
AnyHandler copy{rhs};
|
||||
pimpl_.swap(copy.pimpl_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AnyHandler(AnyHandler&&) = default;
|
||||
AnyHandler&
|
||||
operator=(AnyHandler&&) = default;
|
||||
|
||||
/**
|
||||
* @brief Process incoming JSON by the stored handler
|
||||
*
|
||||
* @param value The JSON to process
|
||||
* @return JSON result or @ref Status on error
|
||||
*/
|
||||
[[nodiscard]] ReturnType
|
||||
process(boost::json::value const& value) const
|
||||
{
|
||||
return pimpl_->process(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process incoming JSON by the stored handler in a provided coroutine
|
||||
*
|
||||
* @param value The JSON to process
|
||||
* @return JSON result or @ref Status on error
|
||||
*/
|
||||
[[nodiscard]] ReturnType
|
||||
process(boost::json::value const& value, Context const& ctx) const
|
||||
{
|
||||
return pimpl_->process(value, ctx);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Concept
|
||||
{
|
||||
virtual ~Concept() = default;
|
||||
|
||||
[[nodiscard]] virtual ReturnType
|
||||
process(boost::json::value const& value, Context const& ctx) const = 0;
|
||||
|
||||
[[nodiscard]] virtual ReturnType
|
||||
process(boost::json::value const& value) const = 0;
|
||||
|
||||
[[nodiscard]] virtual std::unique_ptr<Concept>
|
||||
clone() const = 0;
|
||||
};
|
||||
|
||||
template <typename HandlerType, typename ProcessorType>
|
||||
struct Model : Concept
|
||||
{
|
||||
HandlerType handler;
|
||||
ProcessorType processor;
|
||||
|
||||
Model(HandlerType&& handler) : handler{std::move(handler)}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] ReturnType
|
||||
process(boost::json::value const& value) const override
|
||||
{
|
||||
return processor(handler, value);
|
||||
}
|
||||
|
||||
[[nodiscard]] ReturnType
|
||||
process(boost::json::value const& value, Context const& ctx) const override
|
||||
{
|
||||
return processor(handler, value, &ctx);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Concept>
|
||||
clone() const override
|
||||
{
|
||||
return std::make_unique<Model>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
std::unique_ptr<Concept> pimpl_;
|
||||
};
|
||||
|
||||
} // namespace RPC
|
||||
88
src/rpc/common/Concepts.h
Normal file
88
src/rpc/common/Concepts.h
Normal file
@@ -0,0 +1,88 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/Types.h>
|
||||
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
struct RpcSpec;
|
||||
|
||||
/**
|
||||
* @brief A concept that specifies what a requirement used with @ref FieldSpec
|
||||
* must provide
|
||||
*/
|
||||
// clang-format off
|
||||
template <typename T>
|
||||
concept Requirement = requires(T a) {
|
||||
{ a.verify(boost::json::value{}, std::string{}) } -> std::same_as<MaybeError>;
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* @brief A concept that specifies what a Handler type must provide
|
||||
*
|
||||
* Note that value_from and value_to should be implemented using tag_invoke
|
||||
* as per boost::json documentation for these functions.
|
||||
*/
|
||||
// clang-format off
|
||||
template <typename T>
|
||||
concept ContextProcessWithInput = requires(T a, typename T::Input in, typename T::Output out, Context const& ctx) {
|
||||
{ a.process(in, ctx) } -> std::same_as<HandlerReturnType<decltype(out)>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept ContextProcessWithoutInput = requires(T a, typename T::Output out, Context const& ctx) {
|
||||
{ a.process(ctx) } -> std::same_as<HandlerReturnType<decltype(out)>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept NonContextProcess = requires(T a, typename T::Input in, typename T::Output out) {
|
||||
{ a.process(in) } -> std::same_as<HandlerReturnType<decltype(out)>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HandlerWithInput = requires(T a) {
|
||||
{ a.spec() } -> std::same_as<RpcSpecConstRef>;
|
||||
}
|
||||
and (ContextProcessWithInput<T> or NonContextProcess<T>)
|
||||
and boost::json::has_value_to<typename T::Input>::value;
|
||||
|
||||
template <typename T>
|
||||
concept HandlerWithoutInput = requires(T a, typename T::Output out) {
|
||||
{ a.process() } -> std::same_as<HandlerReturnType<decltype(out)>>;
|
||||
}
|
||||
or ContextProcessWithoutInput<T>;
|
||||
|
||||
template <typename T>
|
||||
concept Handler =
|
||||
(
|
||||
HandlerWithInput<T> or
|
||||
HandlerWithoutInput<T>
|
||||
)
|
||||
and boost::json::has_value_from<typename T::Output>::value;
|
||||
// clang-format on
|
||||
|
||||
} // namespace RPC
|
||||
42
src/rpc/common/Specs.cpp
Normal file
42
src/rpc/common/Specs.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <rpc/common/Specs.h>
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
FieldSpec::validate(boost::json::value const& value) const
|
||||
{
|
||||
return validator_(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
RpcSpec::validate(boost::json::value const& value) const
|
||||
{
|
||||
for (auto const& field : fields_)
|
||||
if (auto ret = field.validate(value); not ret)
|
||||
return Error{ret.error()};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
93
src/rpc/common/Specs.h
Normal file
93
src/rpc/common/Specs.h
Normal file
@@ -0,0 +1,93 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/Concepts.h>
|
||||
#include <rpc/common/Types.h>
|
||||
#include <rpc/common/impl/Factories.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief Represents a Specification for one field of an RPC command
|
||||
*/
|
||||
struct FieldSpec final
|
||||
{
|
||||
/**
|
||||
* @brief Construct a field specification out of a set of requirements
|
||||
*
|
||||
* @tparam Requirements The types of requirements @ref Requirement
|
||||
* @param key The key in a JSON object that the field validates
|
||||
* @param requirements The requirements, each of them have to fulfil
|
||||
* the @ref Requirement concept
|
||||
*/
|
||||
template <Requirement... Requirements>
|
||||
FieldSpec(std::string const& key, Requirements&&... requirements)
|
||||
: validator_{detail::makeFieldValidator<Requirements...>(key, std::forward<Requirements>(requirements)...)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Validates the passed JSON value using the stored requirements
|
||||
*
|
||||
* @param value The JSON value to validate
|
||||
* @return Nothing on success; @ref Status on error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
validate(boost::json::value const& value) const;
|
||||
|
||||
private:
|
||||
std::function<MaybeError(boost::json::value const&)> validator_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a Specification of an entire RPC command
|
||||
*
|
||||
* Note: this should really be all constexpr and handlers would expose
|
||||
* static constexpr RpcSpec spec instead. Maybe some day in the future.
|
||||
*/
|
||||
struct RpcSpec final
|
||||
{
|
||||
/**
|
||||
* @brief Construct a full RPC request specification
|
||||
*
|
||||
* @param fields The fields of the RPC specification @ref FieldSpec
|
||||
*/
|
||||
RpcSpec(std::initializer_list<FieldSpec> fields) : fields_{fields}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Validates the passed JSON value using the stored field specs
|
||||
*
|
||||
* @param value The JSON value to validate
|
||||
* @return Nothing on success; @ref Status on error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
validate(boost::json::value const& value) const;
|
||||
|
||||
private:
|
||||
std::vector<FieldSpec> fields_;
|
||||
};
|
||||
|
||||
} // namespace RPC
|
||||
119
src/rpc/common/Types.h
Normal file
119
src/rpc/common/Types.h
Normal file
@@ -0,0 +1,119 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/Errors.h>
|
||||
#include <util/Expected.h>
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
class WsBase;
|
||||
class SubscriptionManager;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief Return type used for Validators that can return error but don't have
|
||||
* specific value to return
|
||||
*/
|
||||
using MaybeError = util::Expected<void, Status>;
|
||||
|
||||
/**
|
||||
* @brief The type that represents just the error part of @ref MaybeError
|
||||
*/
|
||||
using Error = util::Unexpected<Status>;
|
||||
|
||||
/**
|
||||
* @brief Return type for each individual handler
|
||||
*/
|
||||
template <typename OutputType>
|
||||
using HandlerReturnType = util::Expected<OutputType, Status>;
|
||||
|
||||
/**
|
||||
* @brief The final return type out of RPC engine
|
||||
*/
|
||||
using ReturnType = util::Expected<boost::json::value, Status>;
|
||||
|
||||
struct RpcSpec;
|
||||
struct FieldSpec;
|
||||
|
||||
using RpcSpecConstRef = RpcSpec const&;
|
||||
|
||||
struct VoidOutput
|
||||
{
|
||||
};
|
||||
|
||||
struct Context
|
||||
{
|
||||
// TODO: we shall change yield_context to const yield_context after we
|
||||
// update backend interfaces to use const& yield
|
||||
std::reference_wrapper<boost::asio::yield_context> yield;
|
||||
std::shared_ptr<WsBase> session;
|
||||
bool isAdmin = false;
|
||||
std::string clientIp;
|
||||
};
|
||||
|
||||
using Result = std::variant<Status, boost::json::object>;
|
||||
|
||||
struct AccountCursor
|
||||
{
|
||||
ripple::uint256 index;
|
||||
std::uint32_t hint;
|
||||
|
||||
std::string
|
||||
toString() const
|
||||
{
|
||||
return ripple::strHex(index) + "," + std::to_string(hint);
|
||||
}
|
||||
|
||||
bool
|
||||
isNonZero() const
|
||||
{
|
||||
return index.isNonZero() || hint != 0;
|
||||
}
|
||||
};
|
||||
|
||||
class AnyHandler;
|
||||
|
||||
class HandlerProvider
|
||||
{
|
||||
public:
|
||||
virtual ~HandlerProvider() = default;
|
||||
|
||||
virtual bool
|
||||
contains(std::string const& method) const = 0;
|
||||
|
||||
virtual std::optional<AnyHandler>
|
||||
getHandler(std::string const& command) const = 0;
|
||||
|
||||
virtual bool
|
||||
isClioOnly(std::string const& command) const = 0;
|
||||
};
|
||||
|
||||
inline void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, VoidOutput const&)
|
||||
{
|
||||
jv = boost::json::object{};
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
244
src/rpc/common/Validators.cpp
Normal file
244
src/rpc/common/Validators.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
#include <rpc/common/Validators.h>
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <charconv>
|
||||
#include <string_view>
|
||||
|
||||
namespace RPC::validation {
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
Section::verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
auto const& res = value.at(key.data());
|
||||
|
||||
// if it is not a json object, let other validators fail
|
||||
if (!res.is_object())
|
||||
return {};
|
||||
|
||||
for (auto const& spec : specs)
|
||||
{
|
||||
if (auto const ret = spec.validate(res); not ret)
|
||||
return Error{ret.error()};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
Required::verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "Required field '" + std::string{key} + "' missing"}};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
ValidateArrayAt::verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
if (not value.as_object().at(key.data()).is_array())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
auto const& arr = value.as_object().at(key.data()).as_array();
|
||||
if (idx_ >= arr.size())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
auto const& res = arr.at(idx_);
|
||||
for (auto const& spec : specs_)
|
||||
if (auto const ret = spec.validate(res); not ret)
|
||||
return Error{ret.error()};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
CustomValidator::verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
return validator_(value.as_object().at(key.data()), key);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv)
|
||||
{
|
||||
uint32_t unused;
|
||||
auto [_, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), unused);
|
||||
|
||||
return ec == std::errc();
|
||||
}
|
||||
|
||||
CustomValidator Uint256HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
ripple::uint256 ledgerHash;
|
||||
if (!ledgerHash.parseHex(value.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator LedgerIndexValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
auto err = Error{Status{RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"}};
|
||||
|
||||
if (!value.is_string() && !(value.is_uint64() || value.is_int64()))
|
||||
return err;
|
||||
|
||||
if (value.is_string() && value.as_string() != "validated" && !checkIsU32Numeric(value.as_string().c_str()))
|
||||
return err;
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator AccountValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
// TODO: we are using accountFromStringStrict from RPCHelpers, after we
|
||||
// remove all old handler, this function can be moved to here
|
||||
if (!accountFromStringStrict(value.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcACT_MALFORMED, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator AccountBase58Validator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
auto const account = ripple::parseBase58<ripple::AccountID>(value.as_string().c_str());
|
||||
if (!account || account->isZero())
|
||||
return Error{Status{ClioError::rpcMALFORMED_ADDRESS}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator AccountMarkerValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
// TODO: we are using parseAccountCursor from RPCHelpers, after we
|
||||
// remove all old handler, this function can be moved to here
|
||||
if (!parseAccountCursor(value.as_string().c_str()))
|
||||
{
|
||||
// align with the current error message
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "Malformed cursor"}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CurrencyValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
ripple::Currency currency;
|
||||
if (!ripple::to_currency(currency, value.as_string().c_str()))
|
||||
return Error{Status{ClioError::rpcMALFORMED_CURRENCY, "malformedCurrency"}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator IssuerValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
ripple::AccountID issuer;
|
||||
|
||||
// TODO: need to align with the error
|
||||
if (!ripple::to_issuer(issuer, value.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, fmt::format("Invalid field '{}', bad issuer.", key)}};
|
||||
|
||||
if (issuer == ripple::noAccount())
|
||||
return Error{Status{
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
fmt::format(
|
||||
"Invalid field '{}', bad issuer account "
|
||||
"one.",
|
||||
key)}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator SubscribeStreamValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
static std::unordered_set<std::string> const validStreams = {
|
||||
"ledger", "transactions", "transactions_proposed", "book_changes", "manifests", "validations"};
|
||||
if (!value.is_array())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
|
||||
|
||||
for (auto const& v : value.as_array())
|
||||
{
|
||||
if (!v.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "streamNotString"}};
|
||||
|
||||
if (not validStreams.contains(v.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcSTREAM_MALFORMED}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator SubscribeAccountsValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_array())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
|
||||
|
||||
if (value.as_array().size() == 0)
|
||||
return Error{Status{RippledError::rpcACT_MALFORMED, std::string(key) + " malformed."}};
|
||||
|
||||
for (auto const& v : value.as_array())
|
||||
{
|
||||
auto obj = boost::json::object();
|
||||
auto const keyItem = std::string(key) + "'sItem";
|
||||
|
||||
obj[keyItem] = v;
|
||||
|
||||
if (auto const err = AccountValidator.verify(obj, keyItem); !err)
|
||||
return err;
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace RPC::validation
|
||||
561
src/rpc/common/Validators.h
Normal file
561
src/rpc/common/Validators.h
Normal file
@@ -0,0 +1,561 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/Concepts.h>
|
||||
#include <rpc/common/Specs.h>
|
||||
#include <rpc/common/Types.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace RPC::validation {
|
||||
|
||||
/**
|
||||
* @brief Check that the type is the same as what was expected
|
||||
*
|
||||
* @tparam Expected The expected type that value should be convertible to
|
||||
* @param value The json value to check the type of
|
||||
* @return true if convertible; false otherwise
|
||||
*/
|
||||
template <typename Expected>
|
||||
[[nodiscard]] bool static checkType(boost::json::value const& value)
|
||||
{
|
||||
auto hasError = false;
|
||||
if constexpr (std::is_same_v<Expected, bool>)
|
||||
{
|
||||
if (not value.is_bool())
|
||||
hasError = true;
|
||||
}
|
||||
else if constexpr (std::is_same_v<Expected, std::string>)
|
||||
{
|
||||
if (not value.is_string())
|
||||
hasError = true;
|
||||
}
|
||||
else if constexpr (std::is_same_v<Expected, double> or std::is_same_v<Expected, float>)
|
||||
{
|
||||
if (not value.is_double())
|
||||
hasError = true;
|
||||
}
|
||||
else if constexpr (std::is_same_v<Expected, boost::json::array>)
|
||||
{
|
||||
if (not value.is_array())
|
||||
hasError = true;
|
||||
}
|
||||
else if constexpr (std::is_same_v<Expected, boost::json::object>)
|
||||
{
|
||||
if (not value.is_object())
|
||||
hasError = true;
|
||||
}
|
||||
else if constexpr (std::is_convertible_v<Expected, uint64_t> or std::is_convertible_v<Expected, int64_t>)
|
||||
{
|
||||
if (not value.is_int64() && not value.is_uint64())
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
return not hasError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A meta-validator that acts as a spec for a sub-object/section
|
||||
*/
|
||||
class Section final
|
||||
{
|
||||
std::vector<FieldSpec> specs;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct new section validator from a list of specs
|
||||
*
|
||||
* @param specs List of specs @ref FieldSpec
|
||||
*/
|
||||
explicit Section(std::initializer_list<FieldSpec> specs) : specs{specs}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value representing the section is valid
|
||||
* according to the given specs
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the section from the outer object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A validator that simply requires a field to be present
|
||||
*/
|
||||
struct Required final
|
||||
{
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A validator that forbids a field to be present
|
||||
* If there is a value provided, it will forbid the field only when the value equals
|
||||
* If there is no value provided, it will forbid the field when the field shows up
|
||||
*/
|
||||
template <typename... T>
|
||||
class NotSupported;
|
||||
|
||||
/**
|
||||
* @brief A specialized NotSupported validator that forbids a field to be present when the value equals the given value
|
||||
*/
|
||||
template <typename T>
|
||||
class NotSupported<T> final
|
||||
{
|
||||
T value_;
|
||||
|
||||
public:
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key.data()))
|
||||
{
|
||||
using boost::json::value_to;
|
||||
auto const res = value_to<T>(value.as_object().at(key.data()));
|
||||
if (value_ == res)
|
||||
return Error{Status{
|
||||
RippledError::rpcNOT_SUPPORTED,
|
||||
fmt::format("Not supported field '{}'s value '{}'", std::string{key}, res)}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
NotSupported(T val) : value_(val)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A specialized NotSupported validator that forbids a field to be present
|
||||
*/
|
||||
template <>
|
||||
class NotSupported<> final
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key.data()))
|
||||
return Error{Status{RippledError::rpcNOT_SUPPORTED, "Not supported field '" + std::string{key}}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// deduction guide to avoid having to specify the template arguments
|
||||
template <typename... T>
|
||||
NotSupported(T&&... t) -> NotSupported<T...>;
|
||||
|
||||
/**
|
||||
* @brief Validates that the type of the value is one of the given types
|
||||
*/
|
||||
template <typename... Types>
|
||||
struct Type final
|
||||
{
|
||||
/**
|
||||
* @brief Verify that the JSON value is (one) of specified type(s)
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer
|
||||
* object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
auto const& res = value.as_object().at(key.data());
|
||||
auto const convertible = (checkType<Types>(res) || ...);
|
||||
|
||||
if (not convertible)
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validate that value is between specified min and max
|
||||
*/
|
||||
template <typename Type>
|
||||
class Between final
|
||||
{
|
||||
Type min_;
|
||||
Type max_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator storing min and max values
|
||||
*
|
||||
* @param min
|
||||
* @param max
|
||||
*/
|
||||
explicit Between(Type min, Type max) : min_{min}, max_{max}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is within a certain range
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer
|
||||
* object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key.data()));
|
||||
|
||||
// TODO: may want a way to make this code more generic (e.g. use a free
|
||||
// function that can be overridden for this comparison)
|
||||
if (res < min_ || res > max_)
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validates that the value is equal to the one passed in
|
||||
*/
|
||||
template <typename Type>
|
||||
class EqualTo final
|
||||
{
|
||||
Type original_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator with stored original value
|
||||
*
|
||||
* @param original The original value to store
|
||||
*/
|
||||
explicit EqualTo(Type original) : original_{original}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is equal to the stored original
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer
|
||||
* object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key.data()));
|
||||
if (res != original_)
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Deduction guide to help disambiguate what it means to EqualTo a
|
||||
* "string" without specifying the type.
|
||||
*/
|
||||
EqualTo(char const*)->EqualTo<std::string>;
|
||||
|
||||
/**
|
||||
* @brief Validates that the value is one of the values passed in
|
||||
*/
|
||||
template <typename Type>
|
||||
class OneOf final
|
||||
{
|
||||
std::vector<Type> options_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator with stored options
|
||||
*
|
||||
* @param options The list of allowed options
|
||||
*/
|
||||
explicit OneOf(std::initializer_list<Type> options) : options_{options}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is one of the stored options
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer
|
||||
* object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key.data()));
|
||||
if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Deduction guide to help disambiguate what it means to OneOf a
|
||||
* few "strings" without specifying the type.
|
||||
*/
|
||||
OneOf(std::initializer_list<char const*>)->OneOf<std::string>;
|
||||
|
||||
/**
|
||||
* @brief A meta-validator that specifies a list of specs to run against the
|
||||
* object at the given index in the array
|
||||
*/
|
||||
class ValidateArrayAt final
|
||||
{
|
||||
std::size_t idx_;
|
||||
std::vector<FieldSpec> specs_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a validator that validates the specified element of a
|
||||
* JSON array
|
||||
*
|
||||
* @param idx The index inside the array to validate
|
||||
* @param specs The specifications to validate against
|
||||
*/
|
||||
ValidateArrayAt(std::size_t idx, std::initializer_list<FieldSpec> specs) : idx_{idx}, specs_{specs}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON array element at given index is valid
|
||||
* according the stored specs
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the array from the outer object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-validator that specifies a list of requirements to run against
|
||||
* when the type matches the template parameter
|
||||
*/
|
||||
template <typename Type>
|
||||
class IfType final
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a validator that validates the specs if the type
|
||||
* matches
|
||||
* @param requirements The requirements to validate against
|
||||
*/
|
||||
template <Requirement... Requirements>
|
||||
IfType(Requirements&&... requirements)
|
||||
{
|
||||
validator_ = [... r = std::forward<Requirements>(requirements)](
|
||||
boost::json::value const& j, std::string_view key) -> MaybeError {
|
||||
std::optional<Status> firstFailure = std::nullopt;
|
||||
|
||||
// the check logic is the same as fieldspec
|
||||
// clang-format off
|
||||
([&j, &key, &firstFailure, req = &r]() {
|
||||
if (firstFailure)
|
||||
return;
|
||||
|
||||
if (auto const res = req->verify(j, key); not res)
|
||||
firstFailure = res.error();
|
||||
}(), ...);
|
||||
// clang-format on
|
||||
|
||||
if (firstFailure)
|
||||
return Error{firstFailure.value()};
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the element is valid
|
||||
* according the stored requirements when type matches
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the element from the outer object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail
|
||||
// instead
|
||||
|
||||
if (not checkType<Type>(value.as_object().at(key.data())))
|
||||
return {}; // ignore if type does not match
|
||||
|
||||
return validator_(value, key);
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<MaybeError(boost::json::value const&, std::string_view)> validator_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-validator that wrapp other validator to send the customized
|
||||
* error
|
||||
*/
|
||||
template <typename Requirement>
|
||||
class WithCustomError final
|
||||
{
|
||||
Requirement requirement;
|
||||
Status error;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a validator that calls the given validator "req" and
|
||||
* return customized error "err"
|
||||
*/
|
||||
WithCustomError(Requirement req, Status err) : requirement{std::move(req)}, error{err}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (auto const res = requirement.verify(value, key); not res)
|
||||
return Error{error};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-validator that allows to specify a custom validation function
|
||||
*/
|
||||
class CustomValidator final
|
||||
{
|
||||
std::function<MaybeError(boost::json::value const&, std::string_view)> validator_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a custom validator from any supported callable
|
||||
*
|
||||
* @tparam Fn The type of callable
|
||||
* @param fn The callable/function object
|
||||
*/
|
||||
template <typename Fn>
|
||||
explicit CustomValidator(Fn&& fn) : validator_{std::forward<Fn>(fn)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is valid according to the custom
|
||||
* validation function stored
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer
|
||||
* object
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper function to check if sv is an uint32 number or not
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv);
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for ledger index
|
||||
* LedgerIndex must be a string or int
|
||||
* If the specified LedgerIndex is a string, it's value must be either
|
||||
* "validated" or a valid integer value represented as a string.
|
||||
*/
|
||||
extern CustomValidator LedgerIndexValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for account
|
||||
* Account must be a string and the converted public key is valid
|
||||
*/
|
||||
extern CustomValidator AccountValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for account
|
||||
* Account must be a string and can convert to base58
|
||||
*/
|
||||
extern CustomValidator AccountBase58Validator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for marker
|
||||
* Marker is composed of a comma separated index and start hint. The
|
||||
* former will be read as hex, and the latter can cast to uint64.
|
||||
*/
|
||||
extern CustomValidator AccountMarkerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for uint256 hex string
|
||||
* It must be a string and hex
|
||||
* Transaction index, ledger hash all use this validator
|
||||
*/
|
||||
extern CustomValidator Uint256HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for currency
|
||||
* including standard currency code and token code
|
||||
*/
|
||||
extern CustomValidator CurrencyValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a common used validator for issuer type
|
||||
* It must be a hex string or base58 string
|
||||
*/
|
||||
extern CustomValidator IssuerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a validator for validating valid streams used in
|
||||
* subscribe/unsubscribe
|
||||
*/
|
||||
extern CustomValidator SubscribeStreamValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a validator for validating valid accounts used in
|
||||
* subscribe/unsubscribe
|
||||
*/
|
||||
extern CustomValidator SubscribeAccountsValidator;
|
||||
|
||||
} // namespace RPC::validation
|
||||
42
src/rpc/common/impl/AdminVerificationStrategy.h
Normal file
42
src/rpc/common/impl/AdminVerificationStrategy.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace RPC::detail {
|
||||
|
||||
class IPAdminVerificationStrategy final
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Checks whether request is from a host that is considered authorized as admin.
|
||||
*
|
||||
* @param ip The ip addr of the client
|
||||
* @return true if authorized; false otherwise
|
||||
*/
|
||||
bool
|
||||
isAdmin(std::string_view ip) const
|
||||
{
|
||||
return ip == "127.0.0.1";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace RPC::detail
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user