Compare commits

...

6 Commits

Author SHA1 Message Date
Ayaz Salikhov
8b280e7742 ci: Always upload cache on develop (#2793) 2025-11-19 20:42:18 +00:00
Ayaz Salikhov
7ed30bc40d ci: Don't download ccache on develop branch (#2792) 2025-11-19 19:32:12 +00:00
Sergey Kuznetsov
ac608004bc docs: Fix graceful_period description (#2791) 2025-11-19 19:17:44 +00:00
Alex Kremer
6ab92ca0a6 chore: Enable TSAN in CI (#2785) 2025-11-19 18:06:33 +00:00
Alex Kremer
77387d8f9f chore: Add defines for asan/tsan to conan profile (#2784)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2025-11-19 17:28:10 +00:00
Sergey Kuznetsov
b62cfe949f feat: Graceful shutdown with old web server (#2786)
- Stop accepting connections during graceful shutdown in the old web server
- Stop all the services before Clio exits
- Move cache saving into stop callback
2025-11-19 15:40:33 +00:00
33 changed files with 452 additions and 85 deletions

View File

@@ -4,7 +4,7 @@ import json
LINUX_OS = ["heavy", "heavy-arm64"] LINUX_OS = ["heavy", "heavy-arm64"]
LINUX_CONTAINERS = [ LINUX_CONTAINERS = [
'{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
] ]
LINUX_COMPILERS = ["gcc", "clang"] LINUX_COMPILERS = ["gcc", "clang"]

View File

@@ -49,7 +49,7 @@ jobs:
build_type: [Release, Debug] build_type: [Release, Debug]
container: container:
[ [
'{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }', '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }',
] ]
static: [true] static: [true]
@@ -79,7 +79,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml uses: ./.github/workflows/reusable-build.yml
with: with:
runs_on: heavy runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
conan_profile: gcc conan_profile: gcc
build_type: Debug build_type: Debug
download_ccache: true download_ccache: true
@@ -98,7 +98,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml uses: ./.github/workflows/reusable-build.yml
with: with:
runs_on: heavy runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
conan_profile: gcc conan_profile: gcc
build_type: Release build_type: Release
download_ccache: true download_ccache: true
@@ -115,7 +115,7 @@ jobs:
needs: build-and-test needs: build-and-test
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@@ -21,7 +21,7 @@ jobs:
name: Build Clio / `libXRPL ${{ github.event.client_payload.version }}` name: Build Clio / `libXRPL ${{ github.event.client_payload.version }}`
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -69,7 +69,7 @@ jobs:
needs: build needs: build
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
steps: steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0

View File

@@ -31,7 +31,7 @@ jobs:
if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes') if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes')
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
permissions: permissions:
contents: write contents: write

View File

@@ -18,7 +18,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -43,17 +43,17 @@ jobs:
conan_profile: gcc conan_profile: gcc
build_type: Release build_type: Release
static: true static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
- os: heavy - os: heavy
conan_profile: gcc conan_profile: gcc
build_type: Debug build_type: Debug
static: true static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
- os: heavy - os: heavy
conan_profile: gcc.ubsan conan_profile: gcc.ubsan
build_type: Release build_type: Release
static: false static: false
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
uses: ./.github/workflows/reusable-build-test.yml uses: ./.github/workflows/reusable-build-test.yml
with: with:
@@ -77,7 +77,7 @@ jobs:
include: include:
- os: heavy - os: heavy
conan_profile: clang conan_profile: clang
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
static: true static: true
- os: macos15 - os: macos15
conan_profile: apple-clang conan_profile: apple-clang

View File

@@ -29,7 +29,7 @@ jobs:
conan_profile: gcc conan_profile: gcc
build_type: Release build_type: Release
static: true static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
uses: ./.github/workflows/reusable-build-test.yml uses: ./.github/workflows/reusable-build-test.yml
with: with:

View File

@@ -116,9 +116,8 @@ jobs:
code_coverage: ${{ inputs.code_coverage }} code_coverage: ${{ inputs.code_coverage }}
- name: Restore ccache cache - name: Restore ccache cache
if: ${{ inputs.download_ccache }} if: ${{ inputs.download_ccache && github.ref != 'refs/heads/develop' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: restore_cache
with: with:
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}
key: ${{ steps.cache_key.outputs.key }} key: ${{ steps.cache_key.outputs.key }}
@@ -160,17 +159,14 @@ jobs:
name: build_time_report_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: build_time_report_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build_time_report.txt path: build_time_report.txt
- name: Show ccache's statistics - name: Show ccache's statistics and zero it
if: ${{ inputs.download_ccache }} if: ${{ inputs.download_ccache }}
id: ccache_stats
run: | run: |
ccache -s > /tmp/ccache.stats ccache --show-stats
miss_rate=$(cat /tmp/ccache.stats | grep 'Misses' | head -n1 | sed 's/.*(\(.*\)%).*/\1/') ccache --zero-stats
echo "miss_rate=${miss_rate}" >> $GITHUB_OUTPUT
cat /tmp/ccache.stats
- name: Save ccache cache - name: Save ccache cache
if: ${{ inputs.upload_ccache && github.ref == 'refs/heads/develop' && (steps.restore_cache.outputs.cache-hit != 'true' || steps.ccache_stats.outputs.miss_rate == '100.0') }} if: ${{ inputs.upload_ccache && github.ref == 'refs/heads/develop' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}

View File

@@ -46,7 +46,7 @@ jobs:
release: release:
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View File

@@ -44,14 +44,13 @@ jobs:
uses: ./.github/workflows/reusable-build-test.yml uses: ./.github/workflows/reusable-build-test.yml
with: with:
runs_on: heavy runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8" }' container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
download_ccache: false download_ccache: false
upload_ccache: false upload_ccache: false
conan_profile: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }} conan_profile: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }}
build_type: ${{ matrix.build_type }} build_type: ${{ matrix.build_type }}
static: false static: false
# Currently, both gcc.tsan and clang.tsan unit tests hang run_unit_tests: true
run_unit_tests: ${{ matrix.sanitizer_ext != '.tsan' }}
run_integration_tests: false run_integration_tests: false
upload_clio_server: false upload_clio_server: false
targets: clio_tests clio_integration_tests targets: clio_tests clio_integration_tests

View File

@@ -75,11 +75,6 @@ if (san)
endif () endif ()
target_compile_options(clio_options INTERFACE ${SAN_OPTIMIZATION_FLAG} ${SAN_FLAG} -fno-omit-frame-pointer) target_compile_options(clio_options INTERFACE ${SAN_OPTIMIZATION_FLAG} ${SAN_FLAG} -fno-omit-frame-pointer)
if (san STREQUAL "address")
# ASAN needs these definitions as well as correct b2 flags in conan profile for sanitizers
target_compile_definitions(clio_options INTERFACE BOOST_USE_ASAN=1 BOOST_USE_UCONTEXT=1)
endif ()
target_link_libraries(clio_options INTERFACE ${SAN_FLAG} ${SAN_LIB}) target_link_libraries(clio_options INTERFACE ${SAN_FLAG} ${SAN_LIB})
endif () endif ()

View File

@@ -3,7 +3,11 @@
{% set sanitizer_opt_map = {"asan": "address", "tsan": "thread", "ubsan": "undefined"} %} {% set sanitizer_opt_map = {"asan": "address", "tsan": "thread", "ubsan": "undefined"} %}
{% set sanitizer = sanitizer_opt_map[sani] %} {% set sanitizer = sanitizer_opt_map[sani] %}
{% set sanitizer_b2_flags_map = {"address": "define=BOOST_USE_ASAN=1 context-impl=ucontext address-sanitizer=on", "thread": "thread-sanitizer=on", "undefined": "undefined-sanitizer=on"} %} {% set sanitizer_b2_flags_map = {
"address": "context-impl=ucontext address-sanitizer=norecover",
"thread": "context-impl=ucontext thread-sanitizer=norecover",
"undefined": "undefined-sanitizer=norecover"
} %}
{% set sanitizer_b2_flags_str = sanitizer_b2_flags_map[sanitizer] %} {% set sanitizer_b2_flags_str = sanitizer_b2_flags_map[sanitizer] %}
{% set sanitizer_build_flags_str = "-fsanitize=" ~ sanitizer ~ " -g -O1 -fno-omit-frame-pointer" %} {% set sanitizer_build_flags_str = "-fsanitize=" ~ sanitizer ~ " -g -O1 -fno-omit-frame-pointer" %}
@@ -24,4 +28,10 @@ tools.build:cxxflags+={{ sanitizer_build_flags }}
tools.build:exelinkflags+={{ sanitizer_link_flags }} tools.build:exelinkflags+={{ sanitizer_link_flags }}
tools.build:sharedlinkflags+={{ sanitizer_link_flags }} tools.build:sharedlinkflags+={{ sanitizer_link_flags }}
tools.info.package_id:confs+=["tools.build:cflags", "tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags"] {% if sanitizer == "address" %}
tools.build:defines+=["BOOST_USE_ASAN", "BOOST_USE_UCONTEXT"]
{% elif sanitizer == "thread" %}
tools.build:defines+=["BOOST_USE_TSAN", "BOOST_USE_UCONTEXT"]
{% endif %}
tools.info.package_id:confs+=["tools.build:cflags", "tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"]

View File

@@ -1,6 +1,6 @@
services: services:
clio_develop: clio_develop:
image: ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
volumes: volumes:
- clio_develop_conan_data:/root/.conan2/p - clio_develop_conan_data:/root/.conan2/p
- clio_develop_ccache:/root/.ccache - clio_develop_ccache:/root/.ccache

View File

@@ -191,7 +191,7 @@ Open the `index.html` file in your browser to see the documentation pages.
It is also possible to build Clio using [Docker](https://www.docker.com/) if you don't want to install all the dependencies on your machine. It is also possible to build Clio using [Docker](https://www.docker.com/) if you don't want to install all the dependencies on your machine.
```sh ```sh
docker run -it ghcr.io/xrplf/clio-ci:62369411404eb32b0140603a785ff05e1dc36ce8 docker run -it ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
git clone https://github.com/XRPLF/clio git clone https://github.com/XRPLF/clio
cd clio cd clio
``` ```

View File

@@ -391,7 +391,7 @@ This document provides a list of all available Clio configuration properties in
- **Type**: double - **Type**: double
- **Default value**: `10` - **Default value**: `10`
- **Constraints**: The value must be a positive double number. - **Constraints**: The value must be a positive double number.
- **Description**: The number of milliseconds the server waits to shutdown gracefully. If Clio does not shutdown gracefully after the specified value, it will be killed instead. - **Description**: The number of seconds the server waits to shutdown gracefully. If Clio does not shutdown gracefully after the specified value, it will be killed instead.
### cache.num_diffs ### cache.num_diffs

View File

@@ -25,6 +25,7 @@
#include "data/AmendmentCenter.hpp" #include "data/AmendmentCenter.hpp"
#include "data/BackendFactory.hpp" #include "data/BackendFactory.hpp"
#include "data/LedgerCache.hpp" #include "data/LedgerCache.hpp"
#include "data/LedgerCacheSaver.hpp"
#include "etl/ETLService.hpp" #include "etl/ETLService.hpp"
#include "etl/LoadBalancer.hpp" #include "etl/LoadBalancer.hpp"
#include "etl/NetworkValidatedLedgers.hpp" #include "etl/NetworkValidatedLedgers.hpp"
@@ -98,14 +99,15 @@ ClioApplication::run(bool const useNgWebServer)
auto const threads = config_.get<uint16_t>("io_threads"); auto const threads = config_.get<uint16_t>("io_threads");
LOG(util::LogService::info()) << "Number of io threads = " << threads; LOG(util::LogService::info()) << "Number of io threads = " << threads;
// Similarly we need a context to run ETL on
// In the future we can remove the raw ioc and use ctx instead
// This context should be above ioc because its reference is getting into tasks inside ioc
util::async::CoroExecutionContext ctx{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. // This is not the only io context in the application.
boost::asio::io_context ioc{threads}; boost::asio::io_context ioc{threads};
// Similarly we need a context to run ETL on
// In the future we can remove the raw ioc and use ctx instead
util::async::CoroExecutionContext ctx{threads};
// Rate limiter, to prevent abuse // Rate limiter, to prevent abuse
auto whitelistHandler = web::dosguard::WhitelistHandler{config_}; auto whitelistHandler = web::dosguard::WhitelistHandler{config_};
auto const dosguardWeights = web::dosguard::Weights::make(config_); auto const dosguardWeights = web::dosguard::Weights::make(config_);
@@ -113,21 +115,7 @@ ClioApplication::run(bool const useNgWebServer)
auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard}; auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard};
auto cache = data::LedgerCache{}; auto cache = data::LedgerCache{};
appStopper_.setOnStop([&cache, this](auto&&) { auto cacheSaver = data::LedgerCacheSaver{config_, cache};
// TODO(kuznetsss): move this into Stopper::makeOnStopCallback()
auto const cacheFilePath = config_.maybeValue<std::string>("cache.file.path");
if (not cacheFilePath.has_value()) {
return;
}
LOG(util::LogService::info()) << "Saving ledger cache to " << *cacheFilePath;
if (auto const [success, duration_ms] = util::timed([&]() { return cache.saveToFile(*cacheFilePath); });
success.has_value()) {
LOG(util::LogService::info()) << "Successfully saved ledger cache in " << duration_ms << " ms";
} else {
LOG(util::LogService::error()) << "Error saving LedgerCache to file";
}
});
// Interface to the database // Interface to the database
auto backend = data::makeBackend(config_, cache); auto backend = data::makeBackend(config_, cache);
@@ -208,7 +196,7 @@ ClioApplication::run(bool const useNgWebServer)
} }
appStopper_.setOnStop( appStopper_.setOnStop(
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc) Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, cacheSaver, ioc)
); );
// Blocks until stopped. // Blocks until stopped.
@@ -223,6 +211,9 @@ ClioApplication::run(bool const useNgWebServer)
auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard); auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard);
auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache); auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache);
appStopper_.setOnStop(
Stopper::makeOnStopCallback(*httpServer, *balancer, *etl, *subscriptions, *backend, cacheSaver, ioc)
);
// Blocks until stopped. // Blocks until stopped.
// When stopped, shared_ptrs fall out of scope // When stopped, shared_ptrs fall out of scope

View File

@@ -20,12 +20,13 @@
#pragma once #pragma once
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "data/LedgerCacheSaver.hpp"
#include "etl/ETLServiceInterface.hpp" #include "etl/ETLServiceInterface.hpp"
#include "etl/LoadBalancerInterface.hpp" #include "etl/LoadBalancerInterface.hpp"
#include "feed/SubscriptionManagerInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp"
#include "util/CoroutineGroup.hpp" #include "util/CoroutineGroup.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include "web/ng/Server.hpp" #include "web/interface/Concepts.hpp"
#include <boost/asio/executor_work_guard.hpp> #include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
@@ -71,10 +72,11 @@ public:
* @param etl The ETL service to stop. * @param etl The ETL service to stop.
* @param subscriptions The subscription manager to stop. * @param subscriptions The subscription manager to stop.
* @param backend The backend to stop. * @param backend The backend to stop.
* @param cacheSaver The ledger cache saver
* @param ioc The io_context to stop. * @param ioc The io_context to stop.
* @return The callback to be called on application stop. * @return The callback to be called on application stop.
*/ */
template <web::ng::SomeServer ServerType> template <web::SomeServer ServerType, data::SomeLedgerCacheSaver LedgerCacheSaverType>
static std::function<void(boost::asio::yield_context)> static std::function<void(boost::asio::yield_context)>
makeOnStopCallback( makeOnStopCallback(
ServerType& server, ServerType& server,
@@ -82,10 +84,13 @@ public:
etl::ETLServiceInterface& etl, etl::ETLServiceInterface& etl,
feed::SubscriptionManagerInterface& subscriptions, feed::SubscriptionManagerInterface& subscriptions,
data::BackendInterface& backend, data::BackendInterface& backend,
LedgerCacheSaverType& cacheSaver,
boost::asio::io_context& ioc boost::asio::io_context& ioc
) )
{ {
return [&](boost::asio::yield_context yield) { return [&](boost::asio::yield_context yield) {
cacheSaver.save();
util::CoroutineGroup coroutineGroup{yield}; util::CoroutineGroup coroutineGroup{yield};
coroutineGroup.spawn(yield, [&server](auto innerYield) { coroutineGroup.spawn(yield, [&server](auto innerYield) {
server.stop(innerYield); server.stop(innerYield);
@@ -106,6 +111,8 @@ public:
backend.waitForWritesToFinish(); backend.waitForWritesToFinish();
LOG(util::LogService::info()) << "Backend writes finished"; LOG(util::LogService::info()) << "Backend writes finished";
cacheSaver.waitToFinish();
ioc.stop(); ioc.stop();
LOG(util::LogService::info()) << "io_context stopped"; LOG(util::LogService::info()) << "io_context stopped";

View File

@@ -5,6 +5,7 @@ target_sources(
BackendCounters.cpp BackendCounters.cpp
BackendInterface.cpp BackendInterface.cpp
LedgerCache.cpp LedgerCache.cpp
LedgerCacheSaver.cpp
LedgerHeaderCache.cpp LedgerHeaderCache.cpp
cassandra/impl/Future.cpp cassandra/impl/Future.cpp
cassandra/impl/Cluster.cpp cassandra/impl/Cluster.cpp

View File

@@ -265,7 +265,7 @@ LedgerCache::saveToFile(std::string const& path) const
} }
impl::LedgerCacheFile file{path}; impl::LedgerCacheFile file{path};
std::unique_lock const lock{mtx_}; std::shared_lock const lock{mtx_};
impl::LedgerCacheFile::DataView const data{.latestSeq = latestSeq_, .map = map_, .deleted = deleted_}; impl::LedgerCacheFile::DataView const data{.latestSeq = latestSeq_, .map = map_, .deleted = deleted_};
return file.write(data); return file.write(data);
} }

View File

@@ -145,15 +145,8 @@ public:
void void
waitUntilCacheContainsSeq(uint32_t seq) override; waitUntilCacheContainsSeq(uint32_t seq) override;
/**
* @brief Save the cache to file
* @note This operation takes about 7 seconds and it keeps mtx_ exclusively locked
*
* @param path The file path to save the cache to
* @return An error as a string if any
*/
std::expected<void, std::string> std::expected<void, std::string>
saveToFile(std::string const& path) const; saveToFile(std::string const& path) const override;
std::expected<void, std::string> std::expected<void, std::string>
loadFromFile(std::string const& path, uint32_t minLatestSequence) override; loadFromFile(std::string const& path, uint32_t minLatestSequence) override;

View File

@@ -171,6 +171,16 @@ public:
virtual void virtual void
waitUntilCacheContainsSeq(uint32_t seq) = 0; waitUntilCacheContainsSeq(uint32_t seq) = 0;
/**
* @brief Save the cache to file
* @note This operation takes about 7 seconds and it keeps a shared lock of mtx_
*
* @param path The file path to save the cache to
* @return An error as a string if any
*/
[[nodiscard]] virtual std::expected<void, std::string>
saveToFile(std::string const& path) const = 0;
/** /**
* @brief Load the cache from file * @brief Load the cache from file
* @note This operation takes about 7 seconds and it keeps mtx_ exclusively locked * @note This operation takes about 7 seconds and it keeps mtx_ exclusively locked

View File

@@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, 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 "data/LedgerCacheSaver.hpp"
#include "data/LedgerCacheInterface.hpp"
#include "util/Assert.hpp"
#include "util/Profiler.hpp"
#include "util/log/Logger.hpp"
#include <string>
#include <thread>
namespace data {
LedgerCacheSaver::LedgerCacheSaver(util::config::ClioConfigDefinition const& config, LedgerCacheInterface const& cache)
: cacheFilePath_(config.maybeValue<std::string>("cache.file.path")), cache_(cache)
{
}
LedgerCacheSaver::~LedgerCacheSaver()
{
waitToFinish();
}
void
LedgerCacheSaver::save()
{
ASSERT(not savingThread_.has_value(), "Multiple save() calls are not allowed");
savingThread_ = std::thread([this]() {
if (not cacheFilePath_.has_value()) {
return;
}
LOG(util::LogService::info()) << "Saving ledger cache to " << *cacheFilePath_;
if (auto const [success, durationMs] = util::timed([&]() { return cache_.get().saveToFile(*cacheFilePath_); });
success.has_value()) {
LOG(util::LogService::info()) << "Successfully saved ledger cache in " << durationMs << " ms";
} else {
LOG(util::LogService::error()) << "Error saving LedgerCache to file";
}
});
}
void
LedgerCacheSaver::waitToFinish()
{
if (savingThread_.has_value() and savingThread_->joinable()) {
savingThread_->join();
}
savingThread_.reset();
}
} // namespace data

View File

@@ -0,0 +1,93 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, 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 "data/LedgerCacheInterface.hpp"
#include "util/config/ConfigDefinition.hpp"
#include <concepts>
#include <functional>
#include <optional>
#include <string>
#include <thread>
namespace data {
/**
* @brief A concept for a class that can save ledger cache asynchronously.
*
* This concept defines the interface requirements for any type that manages
* asynchronous saving of ledger cache to persistent storage.
*/
template <typename T>
concept SomeLedgerCacheSaver = requires(T a) {
{ a.save() } -> std::same_as<void>;
{ a.waitToFinish() } -> std::same_as<void>;
};
/**
* @brief Manages asynchronous saving of ledger cache to a file.
*
* This class provides functionality to save the ledger cache to a file in a separate thread,
* allowing the main application to continue without blocking. The file path is configured
* through the application's configuration system.
*/
class LedgerCacheSaver {
std::optional<std::string> cacheFilePath_;
std::reference_wrapper<LedgerCacheInterface const> cache_;
std::optional<std::thread> savingThread_;
public:
/**
* @brief Constructs a LedgerCacheSaver instance.
*
* @param config The configuration object containing the cache file path setting
* @param cache Reference to the ledger cache interface to be saved
*/
LedgerCacheSaver(util::config::ClioConfigDefinition const& config, LedgerCacheInterface const& cache);
/**
* @brief Destructor that ensures the saving thread is properly joined.
*
* Waits for any ongoing save operation to complete before destruction.
*/
~LedgerCacheSaver();
/**
* @brief Initiates an asynchronous save operation of the ledger cache.
*
* Spawns a new thread that saves the ledger cache to the configured file path.
* If no file path is configured, the operation is skipped. Logs the progress
* and result of the save operation.
*/
void
save();
/**
* @brief Waits for the saving thread to complete.
*
* Blocks until the saving operation finishes if a thread is currently active.
* Safe to call multiple times or when no save operation is in progress.
*/
void
waitToFinish();
};
} // namespace data

View File

@@ -19,6 +19,8 @@
#include "util/StopHelper.hpp" #include "util/StopHelper.hpp"
#include "util/Spawn.hpp"
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
@@ -37,7 +39,7 @@ void
StopHelper::asyncWaitForStop(boost::asio::yield_context yield) StopHelper::asyncWaitForStop(boost::asio::yield_context yield)
{ {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::steady_clock::duration::max()}; boost::asio::steady_timer timer{yield.get_executor(), std::chrono::steady_clock::duration::max()};
onStopReady_.connect([&timer]() { timer.cancel(); }); onStopReady_.connect([&]() { util::spawn(yield, [&timer](auto&&) { timer.cancel(); }); });
boost::system::error_code error; boost::system::error_code error;
if (!*stopped_) if (!*stopped_)
timer.async_wait(yield[error]); timer.async_wait(yield[error]);

View File

@@ -36,6 +36,16 @@ class StopHelper {
std::unique_ptr<std::atomic_bool> stopped_ = std::make_unique<std::atomic_bool>(false); std::unique_ptr<std::atomic_bool> stopped_ = std::make_unique<std::atomic_bool>(false);
public: public:
StopHelper() = default;
~StopHelper() = default;
StopHelper(StopHelper&&) = delete;
StopHelper&
operator=(StopHelper&&) = delete;
StopHelper(StopHelper const&) = delete;
StopHelper&
operator=(StopHelper const&) = delete;
/** /**
* @brief Notify that the class is ready to stop. * @brief Notify that the class is ready to stop.
*/ */

View File

@@ -255,7 +255,7 @@ This document provides a list of all available Clio configuration properties in
.value = "The number of worker threads or processes that are responsible for managing and processing " .value = "The number of worker threads or processes that are responsible for managing and processing "
"subscription-based tasks from `rippled`."}, "subscription-based tasks from `rippled`."},
KV{.key = "graceful_period", KV{.key = "graceful_period",
.value = "The number of milliseconds the server waits to shutdown gracefully. If Clio does not shutdown " .value = "The number of seconds the server waits to shutdown gracefully. If Clio does not shutdown "
"gracefully after the specified value, it will be killed instead."}, "gracefully after the specified value, it will be killed instead."},
KV{.key = "cache.num_diffs", KV{.key = "cache.num_diffs",
.value = "The number of cursors generated is the number of changed (without counting deleted) objects in " .value = "The number of cursors generated is the number of changed (without counting deleted) objects in "

View File

@@ -34,6 +34,7 @@
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/asio/socket_base.hpp> #include <boost/asio/socket_base.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp> #include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp> #include <boost/asio/ssl/error.hpp>
#include <boost/asio/strand.hpp> #include <boost/asio/strand.hpp>
@@ -43,6 +44,7 @@
#include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/core/tcp_stream.hpp>
#include <fmt/format.h> #include <fmt/format.h>
#include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <exception> #include <exception>
@@ -221,7 +223,8 @@ template <
template <typename> class PlainSessionType, template <typename> class PlainSessionType,
template <typename> class SslSessionType, template <typename> class SslSessionType,
SomeServerHandler HandlerType> SomeServerHandler HandlerType>
class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> { class Server : public ServerTag,
public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> {
using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this; using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
util::Logger log_{"WebServer"}; util::Logger log_{"WebServer"};
@@ -235,6 +238,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
std::shared_ptr<AdminVerificationStrategy> adminVerification_; std::shared_ptr<AdminVerificationStrategy> adminVerification_;
std::uint32_t maxWsSendingQueueSize_; std::uint32_t maxWsSendingQueueSize_;
std::shared_ptr<ProxyIpResolver> proxyIpResolver_; std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
std::atomic_bool isStopped_{false};
public: public:
/** /**
@@ -308,6 +312,13 @@ public:
doAccept(); doAccept();
} }
/** @brief Stop accepting new connections */
void
stop(boost::asio::yield_context)
{
isStopped_ = true;
}
private: private:
void void
doAccept() doAccept()
@@ -321,6 +332,10 @@ private:
void void
onAccept(boost::beast::error_code ec, tcp::socket socket) onAccept(boost::beast::error_code ec, tcp::socket socket)
{ {
if (isStopped_) {
return;
}
if (!ec) { if (!ec) {
auto ctxRef = auto ctxRef =
ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt; ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;

View File

@@ -22,6 +22,7 @@
#include <boost/beast.hpp> #include <boost/beast.hpp>
#include <boost/beast/core/error.hpp> #include <boost/beast/core/error.hpp>
#include <concepts>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -39,4 +40,14 @@ concept SomeServerHandler =
{ handler(req, ws) }; { handler(req, ws) };
}; };
/**
* @brief A tag class for server to help identify Server in templated code.
*/
struct ServerTag {
virtual ~ServerTag() = default;
};
template <typename T>
concept SomeServer = std::derived_from<T, ServerTag>;
} // namespace web } // namespace web

View File

@@ -23,6 +23,7 @@
#include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigDefinition.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include "web/ProxyIpResolver.hpp" #include "web/ProxyIpResolver.hpp"
#include "web/interface/Concepts.hpp"
#include "web/ng/Connection.hpp" #include "web/ng/Connection.hpp"
#include "web/ng/MessageHandler.hpp" #include "web/ng/MessageHandler.hpp"
#include "web/ng/ProcessingPolicy.hpp" #include "web/ng/ProcessingPolicy.hpp"
@@ -34,7 +35,6 @@
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp> #include <boost/asio/ssl/context.hpp>
#include <concepts>
#include <cstddef> #include <cstddef>
#include <functional> #include <functional>
#include <optional> #include <optional>
@@ -42,16 +42,6 @@
namespace web::ng { namespace web::ng {
/**
* @brief A tag class for server to help identify Server in templated code.
*/
struct ServerTag {
virtual ~ServerTag() = default;
};
template <typename T>
concept SomeServer = std::derived_from<T, ServerTag>;
/** /**
* @brief Web server class. * @brief Web server class.
*/ */

View File

@@ -79,6 +79,9 @@ struct MockLedgerCache : data::LedgerCacheInterface {
MOCK_METHOD(void, waitUntilCacheContainsSeq, (uint32_t), (override)); MOCK_METHOD(void, waitUntilCacheContainsSeq, (uint32_t), (override));
using SaveToFileReturnType = std::expected<void, std::string>;
MOCK_METHOD(SaveToFileReturnType, saveToFile, (std::string const& path), (const, override));
using LoadFromFileReturnType = std::expected<void, std::string>; using LoadFromFileReturnType = std::expected<void, std::string>;
MOCK_METHOD( MOCK_METHOD(
LoadFromFileReturnType, LoadFromFileReturnType,

View File

@@ -11,6 +11,7 @@ target_sources(
data/BackendCountersTests.cpp data/BackendCountersTests.cpp
data/BackendInterfaceTests.cpp data/BackendInterfaceTests.cpp
data/LedgerCacheTests.cpp data/LedgerCacheTests.cpp
data/LedgerCacheSaverTests.cpp
data/cassandra/AsyncExecutorTests.cpp data/cassandra/AsyncExecutorTests.cpp
data/cassandra/ExecutionStrategyTests.cpp data/cassandra/ExecutionStrategyTests.cpp
data/cassandra/LedgerHeaderCacheTests.cpp data/cassandra/LedgerHeaderCacheTests.cpp

View File

@@ -24,7 +24,7 @@
#include "util/MockPrometheus.hpp" #include "util/MockPrometheus.hpp"
#include "util/MockSubscriptionManager.hpp" #include "util/MockSubscriptionManager.hpp"
#include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigDefinition.hpp"
#include "web/ng/Server.hpp" #include "web/interface/Concepts.hpp"
#include <boost/asio/executor_work_guard.hpp> #include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
@@ -61,16 +61,22 @@ TEST_F(StopperTest, stopCalledMultipleTimes)
} }
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest { struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
struct ServerMock : web::ng::ServerTag { struct ServerMock : web::ServerTag {
MOCK_METHOD(void, stop, (boost::asio::yield_context), ()); MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
}; };
struct MockLedgerCacheSaver {
MOCK_METHOD(void, save, ());
MOCK_METHOD(void, waitToFinish, ());
};
protected: protected:
testing::StrictMock<ServerMock> serverMock_; testing::StrictMock<ServerMock> serverMock_;
testing::StrictMock<MockLoadBalancer> loadBalancerMock_; testing::StrictMock<MockLoadBalancer> loadBalancerMock_;
testing::StrictMock<MockETLService> etlServiceMock_; testing::StrictMock<MockETLService> etlServiceMock_;
testing::StrictMock<MockSubscriptionManager> subscriptionManagerMock_; testing::StrictMock<MockSubscriptionManager> subscriptionManagerMock_;
testing::StrictMock<MockBackend> backendMock_{util::config::ClioConfigDefinition{}}; testing::StrictMock<MockBackend> backendMock_{util::config::ClioConfigDefinition{}};
testing::StrictMock<MockLedgerCacheSaver> cacheSaverMock_;
boost::asio::io_context ioContextToStop_; boost::asio::io_context ioContextToStop_;
bool bool
@@ -86,10 +92,17 @@ TEST_F(StopperMakeCallbackTest, makeCallbackTest)
std::thread t{[this]() { ioContextToStop_.run(); }}; std::thread t{[this]() { ioContextToStop_.run(); }};
auto callback = Stopper::makeOnStopCallback( auto callback = Stopper::makeOnStopCallback(
serverMock_, loadBalancerMock_, etlServiceMock_, subscriptionManagerMock_, backendMock_, ioContextToStop_ serverMock_,
loadBalancerMock_,
etlServiceMock_,
subscriptionManagerMock_,
backendMock_,
cacheSaverMock_,
ioContextToStop_
); );
testing::Sequence const s1, s2; testing::Sequence const s1, s2;
EXPECT_CALL(cacheSaverMock_, save).InSequence(s1).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
EXPECT_CALL(serverMock_, stop).InSequence(s1).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); }); EXPECT_CALL(serverMock_, stop).InSequence(s1).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
EXPECT_CALL(loadBalancerMock_, stop).InSequence(s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); }); EXPECT_CALL(loadBalancerMock_, stop).InSequence(s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
EXPECT_CALL(etlServiceMock_, stop).InSequence(s1, s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); }); EXPECT_CALL(etlServiceMock_, stop).InSequence(s1, s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
@@ -99,6 +112,7 @@ TEST_F(StopperMakeCallbackTest, makeCallbackTest)
EXPECT_CALL(backendMock_, waitForWritesToFinish).InSequence(s1, s2).WillOnce([this]() { EXPECT_CALL(backendMock_, waitForWritesToFinish).InSequence(s1, s2).WillOnce([this]() {
EXPECT_FALSE(isContextStopped()); EXPECT_FALSE(isContextStopped());
}); });
EXPECT_CALL(cacheSaverMock_, waitToFinish).InSequence(s1).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
runSpawn([&](boost::asio::yield_context yield) { runSpawn([&](boost::asio::yield_context yield) {
callback(yield); callback(yield);

View File

@@ -0,0 +1,156 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, 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 "data/LedgerCacheSaver.hpp"
#include "util/MockAssert.hpp"
#include "util/MockLedgerCache.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <expected>
#include <semaphore>
#include <string>
#include <thread>
using namespace data;
using namespace util::config;
struct LedgerCacheSaverTest : virtual testing::Test {
testing::StrictMock<MockLedgerCache> cache;
constexpr static auto kFILE_PATH = "./cache.bin";
static ClioConfigDefinition
generateConfig(bool cacheFilePathHasValue)
{
auto config = ClioConfigDefinition{{
{"cache.file.path", ConfigValue{ConfigType::String}.optional()},
}};
ConfigFileJson jsonFile{boost::json::object{}};
if (cacheFilePathHasValue) {
auto const jsonObject =
boost::json::parse(fmt::format(R"JSON({{"cache": {{"file": {{"path": "{}"}}}}}})JSON", kFILE_PATH))
.as_object();
jsonFile = ConfigFileJson{jsonObject};
}
auto const errors = config.parse(jsonFile);
EXPECT_FALSE(errors.has_value());
return config;
}
};
TEST_F(LedgerCacheSaverTest, SaveSuccessfully)
{
auto const config = generateConfig(true);
LedgerCacheSaver saver{config, cache};
EXPECT_CALL(cache, saveToFile(kFILE_PATH)).WillOnce(testing::Return(std::expected<void, std::string>{}));
saver.save();
saver.waitToFinish();
}
TEST_F(LedgerCacheSaverTest, SaveWithError)
{
auto const config = generateConfig(true);
LedgerCacheSaver saver{config, cache};
EXPECT_CALL(cache, saveToFile(kFILE_PATH))
.WillOnce(testing::Return(std::expected<void, std::string>(std::unexpected("Failed to save"))));
saver.save();
saver.waitToFinish();
}
TEST_F(LedgerCacheSaverTest, NoSaveWhenPathNotConfigured)
{
auto const config = generateConfig(false);
LedgerCacheSaver saver{config, cache};
saver.save();
saver.waitToFinish();
}
TEST_F(LedgerCacheSaverTest, DestructorWaitsForCompletion)
{
auto const config = generateConfig(true);
std::binary_semaphore semaphore{1};
std::atomic_bool saveCompleted{false};
EXPECT_CALL(cache, saveToFile(kFILE_PATH)).WillOnce([&]() {
semaphore.release();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
saveCompleted = true;
return std::expected<void, std::string>{};
});
{
LedgerCacheSaver saver{config, cache};
saver.save();
EXPECT_TRUE(semaphore.try_acquire_for(std::chrono::seconds{5}));
}
EXPECT_TRUE(saveCompleted);
}
TEST_F(LedgerCacheSaverTest, WaitToFinishCanBeCalledMultipleTimes)
{
auto const config = generateConfig(true);
LedgerCacheSaver saver{config, cache};
EXPECT_CALL(cache, saveToFile(kFILE_PATH));
saver.save();
saver.waitToFinish();
EXPECT_NO_THROW(saver.waitToFinish());
}
TEST_F(LedgerCacheSaverTest, WaitToFinishWithoutSaveIsSafe)
{
auto const config = generateConfig(true);
LedgerCacheSaver saver{config, cache};
EXPECT_NO_THROW(saver.waitToFinish());
}
struct LedgerCacheSaverAssertTest : LedgerCacheSaverTest, common::util::WithMockAssert {};
TEST_F(LedgerCacheSaverAssertTest, MultipleSavesNotAllowed)
{
auto const config = generateConfig(true);
LedgerCacheSaver saver{config, cache};
EXPECT_CALL(cache, saveToFile(kFILE_PATH));
saver.save();
EXPECT_CLIO_ASSERT_FAIL({ saver.save(); });
saver.waitToFinish();
}