mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	@@ -59,7 +59,9 @@ function grep_code {
 | 
			
		||||
    grep -l "${1}" ${sources} -r --include \*.hpp --include \*.cpp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
			
		||||
GNU_SED=$(sed --version 2>&1 | grep -q 'GNU' && echo true || echo false)
 | 
			
		||||
 | 
			
		||||
if [[ "$GNU_SED" == "false" ]]; then # macOS sed
 | 
			
		||||
    # make all includes to be <...> style
 | 
			
		||||
    grep_code '#include ".*"' | xargs sed -i '' -E 's|#include "(.*)"|#include <\1>|g'
 | 
			
		||||
 | 
			
		||||
@@ -83,9 +85,10 @@ first=$(git diff $sources $cmake_files)
 | 
			
		||||
find $sources -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.ipp' \) -print0 | xargs -0 $formatter
 | 
			
		||||
cmake-format -i $cmake_files
 | 
			
		||||
second=$(git diff $sources $cmake_files)
 | 
			
		||||
changes=$(diff <(echo "$first") <(echo "$second") | wc -l | sed -e 's/^[[:space:]]*//')
 | 
			
		||||
changes=$(diff <(echo "$first") <(echo "$second"))
 | 
			
		||||
changes_number=$(echo -n "$changes" | wc -l | sed -e 's/^[[:space:]]*//')
 | 
			
		||||
 | 
			
		||||
if [ "$changes" != "0" ]; then
 | 
			
		||||
if [ "$changes_number" != "0" ]; then
 | 
			
		||||
    cat <<\EOF
 | 
			
		||||
 | 
			
		||||
                                   WARNING
 | 
			
		||||
@@ -95,5 +98,8 @@ if [ "$changes" != "0" ]; then
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
EOF
 | 
			
		||||
    if [[ "$1" == "--diff" ]]; then
 | 
			
		||||
        echo "$changes"
 | 
			
		||||
    fi
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,7 +21,7 @@ jobs:
 | 
			
		||||
      - name: Run formatters
 | 
			
		||||
        id: run_formatters
 | 
			
		||||
        run: |
 | 
			
		||||
          ./.githooks/check-format
 | 
			
		||||
          ./.githooks/check-format --diff
 | 
			
		||||
        shell: bash
 | 
			
		||||
  
 | 
			
		||||
  check_docs:
 | 
			
		||||
 
 | 
			
		||||
@@ -235,7 +235,8 @@ BackendInterface::fetchBookOffers(
 | 
			
		||||
    LOG(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"
 | 
			
		||||
                      << " 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"
 | 
			
		||||
 
 | 
			
		||||
@@ -792,8 +792,8 @@ public:
 | 
			
		||||
    void
 | 
			
		||||
    writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override
 | 
			
		||||
    {
 | 
			
		||||
        LOG(log_.trace()) << "Writing successor. key = " << key.size() << " bytes. " << " seq = " << std::to_string(seq)
 | 
			
		||||
                          << " successor = " << successor.size() << " bytes.";
 | 
			
		||||
        LOG(log_.trace()) << "Writing successor. key = " << key.size() << " bytes. "
 | 
			
		||||
                          << " seq = " << std::to_string(seq) << " successor = " << successor.size() << " bytes.";
 | 
			
		||||
        ASSERT(!key.empty(), "Key must not be empty");
 | 
			
		||||
        ASSERT(!successor.empty(), "Successor must not be empty");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -484,7 +484,8 @@ private:
 | 
			
		||||
        {
 | 
			
		||||
            std::unique_lock<std::mutex> lck(throttleMutex_);
 | 
			
		||||
            if (!canAddWriteRequest()) {
 | 
			
		||||
                LOG(log_.trace()) << "Max outstanding requests reached. " << "Waiting for other requests to finish";
 | 
			
		||||
                LOG(log_.trace()) << "Max outstanding requests reached. "
 | 
			
		||||
                                  << "Waiting for other requests to finish";
 | 
			
		||||
                throttleCv_.wait(lck, [this]() { return canAddWriteRequest(); });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,13 @@ add_library(clio_etl)
 | 
			
		||||
 | 
			
		||||
target_sources(
 | 
			
		||||
  clio_etl
 | 
			
		||||
  PRIVATE NFTHelpers.cpp
 | 
			
		||||
  PRIVATE CacheLoaderSettings.cpp
 | 
			
		||||
          ETLHelpers.cpp
 | 
			
		||||
          ETLService.cpp
 | 
			
		||||
          ETLState.cpp
 | 
			
		||||
          LoadBalancer.cpp
 | 
			
		||||
          CacheLoaderSettings.cpp
 | 
			
		||||
          NetworkValidatedLedgers.cpp
 | 
			
		||||
          NFTHelpers.cpp
 | 
			
		||||
          Source.cpp
 | 
			
		||||
          impl/ForwardingCache.cpp
 | 
			
		||||
          impl/ForwardingSource.cpp
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								src/etl/ETLHelpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/etl/ETLHelpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/ETLHelpers.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
 | 
			
		||||
#include <ripple/basics/base_uint.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
std::vector<ripple::uint256>
 | 
			
		||||
getMarkers(size_t numMarkers)
 | 
			
		||||
{
 | 
			
		||||
    ASSERT(numMarkers <= 256, "Number of markers must be <= 256. Got: {}", numMarkers);
 | 
			
		||||
 | 
			
		||||
    unsigned char const incr = 256 / numMarkers;
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> markers;
 | 
			
		||||
    markers.reserve(numMarkers);
 | 
			
		||||
    ripple::uint256 base{0};
 | 
			
		||||
    for (size_t i = 0; i < numMarkers; ++i) {
 | 
			
		||||
        markers.push_back(base);
 | 
			
		||||
        base.data()[0] += incr;
 | 
			
		||||
    }
 | 
			
		||||
    return markers;
 | 
			
		||||
}
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
@@ -20,11 +20,8 @@
 | 
			
		||||
/** @file */
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
 | 
			
		||||
#include <ripple/basics/base_uint.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <condition_variable>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
@@ -35,83 +32,6 @@
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
/**
 | 
			
		||||
 * @brief This datastructure is used to keep track of the sequence of the most recent ledger validated by the network.
 | 
			
		||||
 *
 | 
			
		||||
 * There are two methods that will wait until certain conditions are met. This datastructure is able to be "stopped".
 | 
			
		||||
 * When the datastructure is stopped, any threads currently waiting are unblocked.
 | 
			
		||||
 * Any later calls to methods of this datastructure will not wait. Once the datastructure is stopped, the datastructure
 | 
			
		||||
 * remains stopped for the rest of its lifetime.
 | 
			
		||||
 */
 | 
			
		||||
class NetworkValidatedLedgers {
 | 
			
		||||
    // max sequence validated by network
 | 
			
		||||
    std::optional<uint32_t> max_;
 | 
			
		||||
 | 
			
		||||
    mutable std::mutex m_;
 | 
			
		||||
    std::condition_variable cv_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A factory function for NetworkValidatedLedgers
 | 
			
		||||
     *
 | 
			
		||||
     * @return A shared pointer to a new instance of NetworkValidatedLedgers
 | 
			
		||||
     */
 | 
			
		||||
    static std::shared_ptr<NetworkValidatedLedgers>
 | 
			
		||||
    make_ValidatedLedgers()
 | 
			
		||||
    {
 | 
			
		||||
        return std::make_shared<NetworkValidatedLedgers>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Notify the datastructure that idx has been validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param idx Sequence validated by network
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    push(uint32_t idx)
 | 
			
		||||
    {
 | 
			
		||||
        std::lock_guard const lck(m_);
 | 
			
		||||
        if (!max_ || idx > *max_)
 | 
			
		||||
            max_ = idx;
 | 
			
		||||
        cv_.notify_all();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get most recently validated sequence.
 | 
			
		||||
     *
 | 
			
		||||
     * If no ledgers are known to have been validated, this function waits until the next ledger is validated
 | 
			
		||||
     *
 | 
			
		||||
     * @return Sequence of most recently validated ledger. empty optional if the datastructure has been stopped
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<uint32_t>
 | 
			
		||||
    getMostRecent()
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lck(m_);
 | 
			
		||||
        cv_.wait(lck, [this]() { return max_; });
 | 
			
		||||
        return max_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Waits for the sequence to be validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence The sequence to wait for
 | 
			
		||||
     * @param maxWaitMs Maximum time to wait for the sequence to be validated. If empty, wait indefinitely
 | 
			
		||||
     * @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 = {})
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lck(m_);
 | 
			
		||||
        auto pred = [sequence, this]() -> bool { return (max_ && sequence <= *max_); };
 | 
			
		||||
        if (maxWaitMs) {
 | 
			
		||||
            cv_.wait_for(lck, std::chrono::milliseconds(*maxWaitMs));
 | 
			
		||||
        } else {
 | 
			
		||||
            cv_.wait(lck, pred);
 | 
			
		||||
        }
 | 
			
		||||
        return pred();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: does the note make sense? lockfree queues provide the same blocking behaviour just without mutex, don't they?
 | 
			
		||||
/**
 | 
			
		||||
@@ -228,20 +148,7 @@ public:
 | 
			
		||||
 * @param numMarkers Total markers to partition for
 | 
			
		||||
 * @return The markers
 | 
			
		||||
 */
 | 
			
		||||
inline std::vector<ripple::uint256>
 | 
			
		||||
getMarkers(size_t numMarkers)
 | 
			
		||||
{
 | 
			
		||||
    ASSERT(numMarkers <= 256, "Number of markers must be <= 256. Got: {}", numMarkers);
 | 
			
		||||
std::vector<ripple::uint256>
 | 
			
		||||
getMarkers(size_t numMarkers);
 | 
			
		||||
 | 
			
		||||
    unsigned char const incr = 256 / numMarkers;
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> markers;
 | 
			
		||||
    markers.reserve(numMarkers);
 | 
			
		||||
    ripple::uint256 base{0};
 | 
			
		||||
    for (size_t i = 0; i < numMarkers; ++i) {
 | 
			
		||||
        markers.push_back(base);
 | 
			
		||||
        base.data()[0] += incr;
 | 
			
		||||
    }
 | 
			
		||||
    return markers;
 | 
			
		||||
}
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "data/LedgerCache.hpp"
 | 
			
		||||
#include "etl/CorruptionDetector.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Constants.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
@@ -263,9 +265,9 @@ ETLService::ETLService(
 | 
			
		||||
    util::Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<SubscriptionManagerType> subscriptions,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<LoadBalancerType> balancer,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersType> ledgers
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> ledgers
 | 
			
		||||
)
 | 
			
		||||
    : backend_(backend)
 | 
			
		||||
    , loadBalancer_(balancer)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
#include "etl/impl/LedgerLoader.hpp"
 | 
			
		||||
#include "etl/impl/LedgerPublisher.hpp"
 | 
			
		||||
#include "etl/impl/Transformer.hpp"
 | 
			
		||||
#include "feed/SubscriptionManager.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
@@ -77,16 +77,14 @@ namespace etl {
 | 
			
		||||
 */
 | 
			
		||||
class ETLService {
 | 
			
		||||
    // TODO: make these template parameters in ETLService
 | 
			
		||||
    using SubscriptionManagerType = feed::SubscriptionManager;
 | 
			
		||||
    using LoadBalancerType = LoadBalancer;
 | 
			
		||||
    using NetworkValidatedLedgersType = NetworkValidatedLedgers;
 | 
			
		||||
    using DataPipeType = etl::impl::ExtractionDataPipe<org::xrpl::rpc::v1::GetLedgerResponse>;
 | 
			
		||||
    using CacheType = data::LedgerCache;
 | 
			
		||||
    using CacheLoaderType = etl::CacheLoader<CacheType>;
 | 
			
		||||
    using LedgerFetcherType = etl::impl::LedgerFetcher<LoadBalancerType>;
 | 
			
		||||
    using ExtractorType = etl::impl::Extractor<DataPipeType, NetworkValidatedLedgersType, LedgerFetcherType>;
 | 
			
		||||
    using ExtractorType = etl::impl::Extractor<DataPipeType, LedgerFetcherType>;
 | 
			
		||||
    using LedgerLoaderType = etl::impl::LedgerLoader<LoadBalancerType, LedgerFetcherType>;
 | 
			
		||||
    using LedgerPublisherType = etl::impl::LedgerPublisher<SubscriptionManagerType, CacheType>;
 | 
			
		||||
    using LedgerPublisherType = etl::impl::LedgerPublisher<CacheType>;
 | 
			
		||||
    using AmendmentBlockHandlerType = etl::impl::AmendmentBlockHandler<>;
 | 
			
		||||
    using TransformerType =
 | 
			
		||||
        etl::impl::Transformer<DataPipeType, LedgerLoaderType, LedgerPublisherType, AmendmentBlockHandlerType>;
 | 
			
		||||
@@ -95,7 +93,7 @@ class ETLService {
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend_;
 | 
			
		||||
    std::shared_ptr<LoadBalancerType> loadBalancer_;
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersType> networkValidatedLedgers_;
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> networkValidatedLedgers_;
 | 
			
		||||
 | 
			
		||||
    std::uint32_t extractorThreads_ = 1;
 | 
			
		||||
    std::thread worker_;
 | 
			
		||||
@@ -128,9 +126,9 @@ public:
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> subscriptions,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        std::shared_ptr<LoadBalancerType> balancer,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersType> ledgers
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> ledgers
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -151,9 +149,9 @@ public:
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> subscriptions,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        std::shared_ptr<LoadBalancerType> balancer,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersType> ledgers
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> ledgers
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        auto etl = std::make_shared<ETLService>(config, ioc, backend, subscriptions, balancer, ledgers);
 | 
			
		||||
 
 | 
			
		||||
@@ -27,21 +27,23 @@
 | 
			
		||||
#include <ripple/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
 | 
			
		||||
ETLState
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<ETLState>, boost::json::value const& jv)
 | 
			
		||||
std::optional<ETLState>
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<std::optional<ETLState>>, boost::json::value const& jv)
 | 
			
		||||
{
 | 
			
		||||
    ETLState state;
 | 
			
		||||
    auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
    if (!jsonObject.contains(JS(error))) {
 | 
			
		||||
        if (jsonObject.contains(JS(result)) && jsonObject.at(JS(result)).as_object().contains(JS(info))) {
 | 
			
		||||
            auto const rippledInfo = jsonObject.at(JS(result)).as_object().at(JS(info)).as_object();
 | 
			
		||||
            if (rippledInfo.contains(JS(network_id)))
 | 
			
		||||
                state.networkID.emplace(boost::json::value_to<int64_t>(rippledInfo.at(JS(network_id))));
 | 
			
		||||
        }
 | 
			
		||||
    if (jsonObject.contains(JS(error)))
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(result)) && jsonObject.at(JS(result)).as_object().contains(JS(info))) {
 | 
			
		||||
        auto const rippledInfo = jsonObject.at(JS(result)).as_object().at(JS(info)).as_object();
 | 
			
		||||
        if (rippledInfo.contains(JS(network_id)))
 | 
			
		||||
            state.networkID.emplace(boost::json::value_to<int64_t>(rippledInfo.at(JS(network_id))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return state;
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ struct ETLState {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (serverInfoRippled)
 | 
			
		||||
            return boost::json::value_to<ETLState>(boost::json::value(*serverInfoRippled));
 | 
			
		||||
            return boost::json::value_to<std::optional<ETLState>>(boost::json::value(*serverInfoRippled));
 | 
			
		||||
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
    }
 | 
			
		||||
@@ -63,7 +63,7 @@ struct ETLState {
 | 
			
		||||
 * @param jv The json value to convert
 | 
			
		||||
 * @return The ETLState
 | 
			
		||||
 */
 | 
			
		||||
ETLState
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<ETLState>, boost::json::value const& jv);
 | 
			
		||||
std::optional<ETLState>
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<std::optional<ETLState>>, boost::json::value const& jv);
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,10 @@
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "etl/ETLService.hpp"
 | 
			
		||||
#include "etl/ETLState.hpp"
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Constants.hpp"
 | 
			
		||||
#include "util/Random.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
@@ -57,19 +58,23 @@ LoadBalancer::make_LoadBalancer(
 | 
			
		||||
    Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgers> validatedLedgers
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    SourceFactory sourceFactory
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    return std::make_shared<LoadBalancer>(config, ioc, backend, subscriptions, validatedLedgers);
 | 
			
		||||
    return std::make_shared<LoadBalancer>(
 | 
			
		||||
        config, ioc, std::move(backend), std::move(subscriptions), std::move(validatedLedgers), std::move(sourceFactory)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LoadBalancer::LoadBalancer(
 | 
			
		||||
    Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgers> validatedLedgers
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    SourceFactory sourceFactory
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    auto const forwardingCacheTimeout = config.valueOr<float>("forwarding_cache_timeout", 0.f);
 | 
			
		||||
@@ -81,7 +86,8 @@ LoadBalancer::LoadBalancer(
 | 
			
		||||
 | 
			
		||||
    static constexpr std::uint32_t MAX_DOWNLOAD = 256;
 | 
			
		||||
    if (auto value = config.maybeValue<uint32_t>("num_markers"); value) {
 | 
			
		||||
        downloadRanges_ = std::clamp(*value, 1u, MAX_DOWNLOAD);
 | 
			
		||||
        ASSERT(*value > 0 and *value <= MAX_DOWNLOAD, "'num_markers' value in config must be in range 1-256");
 | 
			
		||||
        downloadRanges_ = *value;
 | 
			
		||||
    } else if (backend->fetchLedgerRange()) {
 | 
			
		||||
        downloadRanges_ = 4;
 | 
			
		||||
    }
 | 
			
		||||
@@ -98,7 +104,7 @@ LoadBalancer::LoadBalancer(
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    for (auto const& entry : config.array("etl_sources")) {
 | 
			
		||||
        auto source = make_Source(
 | 
			
		||||
        auto source = sourceFactory(
 | 
			
		||||
            entry,
 | 
			
		||||
            ioc,
 | 
			
		||||
            backend,
 | 
			
		||||
@@ -113,12 +119,12 @@ LoadBalancer::LoadBalancer(
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // checking etl node validity
 | 
			
		||||
        auto const stateOpt = ETLState::fetchETLStateFromSource(source);
 | 
			
		||||
        auto const stateOpt = ETLState::fetchETLStateFromSource(*source);
 | 
			
		||||
 | 
			
		||||
        if (!stateOpt) {
 | 
			
		||||
            checkOnETLFailure(fmt::format(
 | 
			
		||||
                "Failed to fetch ETL state from source = {} Please check the configuration and network",
 | 
			
		||||
                source.toString()
 | 
			
		||||
                source->toString()
 | 
			
		||||
            ));
 | 
			
		||||
        } else if (etlState_ && etlState_->networkID && stateOpt->networkID &&
 | 
			
		||||
                   etlState_->networkID != stateOpt->networkID) {
 | 
			
		||||
@@ -132,7 +138,7 @@ LoadBalancer::LoadBalancer(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sources_.push_back(std::move(source));
 | 
			
		||||
        LOG(log_.info()) << "Added etl source - " << sources_.back().toString();
 | 
			
		||||
        LOG(log_.info()) << "Added etl source - " << sources_.back()->toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (sources_.empty())
 | 
			
		||||
@@ -140,8 +146,8 @@ LoadBalancer::LoadBalancer(
 | 
			
		||||
 | 
			
		||||
    // This is made separate from source creation to prevent UB in case one of the sources will call
 | 
			
		||||
    // chooseForwardingSource while we are still filling the sources_ vector
 | 
			
		||||
    for (auto& source : sources_) {
 | 
			
		||||
        source.run();
 | 
			
		||||
    for (auto const& source : sources_) {
 | 
			
		||||
        source->run();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -150,53 +156,57 @@ LoadBalancer::~LoadBalancer()
 | 
			
		||||
    sources_.clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::pair<std::vector<std::string>, bool>
 | 
			
		||||
LoadBalancer::loadInitialLedger(uint32_t sequence, bool cacheOnly)
 | 
			
		||||
std::vector<std::string>
 | 
			
		||||
LoadBalancer::loadInitialLedger(uint32_t sequence, bool cacheOnly, std::chrono::steady_clock::duration retryAfter)
 | 
			
		||||
{
 | 
			
		||||
    std::vector<std::string> response;
 | 
			
		||||
    auto const success = execute(
 | 
			
		||||
    execute(
 | 
			
		||||
        [this, &response, &sequence, cacheOnly](auto& source) {
 | 
			
		||||
            auto [data, res] = source.loadInitialLedger(sequence, downloadRanges_, cacheOnly);
 | 
			
		||||
            auto [data, res] = source->loadInitialLedger(sequence, downloadRanges_, cacheOnly);
 | 
			
		||||
 | 
			
		||||
            if (!res) {
 | 
			
		||||
                LOG(log_.error()) << "Failed to download initial ledger. Sequence = " << sequence
 | 
			
		||||
                                  << " source = " << source.toString();
 | 
			
		||||
                LOG(log_.error()) << "Failed to download initial ledger."
 | 
			
		||||
                                  << " Sequence = " << sequence << " source = " << source->toString();
 | 
			
		||||
            } else {
 | 
			
		||||
                response = std::move(data);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return res;
 | 
			
		||||
        },
 | 
			
		||||
        sequence
 | 
			
		||||
        sequence,
 | 
			
		||||
        retryAfter
 | 
			
		||||
    );
 | 
			
		||||
    return {std::move(response), success};
 | 
			
		||||
    return response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LoadBalancer::OptionalGetLedgerResponseType
 | 
			
		||||
LoadBalancer::fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObjectNeighbors)
 | 
			
		||||
LoadBalancer::fetchLedger(
 | 
			
		||||
    uint32_t ledgerSequence,
 | 
			
		||||
    bool getObjects,
 | 
			
		||||
    bool getObjectNeighbors,
 | 
			
		||||
    std::chrono::steady_clock::duration retryAfter
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    GetLedgerResponseType response;
 | 
			
		||||
    bool const success = execute(
 | 
			
		||||
    execute(
 | 
			
		||||
        [&response, ledgerSequence, getObjects, getObjectNeighbors, log = log_](auto& source) {
 | 
			
		||||
            auto [status, data] = source.fetchLedger(ledgerSequence, getObjects, getObjectNeighbors);
 | 
			
		||||
            auto [status, data] = source->fetchLedger(ledgerSequence, getObjects, getObjectNeighbors);
 | 
			
		||||
            response = std::move(data);
 | 
			
		||||
            if (status.ok() && response.validated()) {
 | 
			
		||||
                LOG(log.info()) << "Successfully fetched ledger = " << ledgerSequence
 | 
			
		||||
                                << " from source = " << source.toString();
 | 
			
		||||
                                << " from source = " << source->toString();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LOG(log.warn()) << "Could not fetch ledger " << ledgerSequence << ", Reply: " << response.DebugString()
 | 
			
		||||
                            << ", error_code: " << status.error_code() << ", error_msg: " << status.error_message()
 | 
			
		||||
                            << ", source = " << source.toString();
 | 
			
		||||
                            << ", source = " << source->toString();
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        ledgerSequence
 | 
			
		||||
        ledgerSequence,
 | 
			
		||||
        retryAfter
 | 
			
		||||
    );
 | 
			
		||||
    if (success) {
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
    return {};
 | 
			
		||||
    return response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<boost::json::object>
 | 
			
		||||
@@ -212,15 +222,14 @@ LoadBalancer::forwardToRippled(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::size_t sourceIdx = 0;
 | 
			
		||||
    if (!sources_.empty())
 | 
			
		||||
        sourceIdx = util::Random::uniform(0ul, sources_.size() - 1);
 | 
			
		||||
    ASSERT(not sources_.empty(), "ETL sources must be configured to forward requests.");
 | 
			
		||||
    std::size_t sourceIdx = util::Random::uniform(0ul, sources_.size() - 1);
 | 
			
		||||
 | 
			
		||||
    auto numAttempts = 0u;
 | 
			
		||||
 | 
			
		||||
    std::optional<boost::json::object> response;
 | 
			
		||||
    while (numAttempts < sources_.size()) {
 | 
			
		||||
        if (auto res = sources_[sourceIdx].forwardToRippled(request, clientIp, yield)) {
 | 
			
		||||
        if (auto res = sources_[sourceIdx]->forwardToRippled(request, clientIp, yield)) {
 | 
			
		||||
            response = std::move(res);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
@@ -240,42 +249,41 @@ LoadBalancer::toJson() const
 | 
			
		||||
{
 | 
			
		||||
    boost::json::array ret;
 | 
			
		||||
    for (auto& src : sources_)
 | 
			
		||||
        ret.push_back(src.toJson());
 | 
			
		||||
        ret.push_back(src->toJson());
 | 
			
		||||
 | 
			
		||||
    return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template <typename Func>
 | 
			
		||||
bool
 | 
			
		||||
LoadBalancer::execute(Func f, uint32_t ledgerSequence)
 | 
			
		||||
void
 | 
			
		||||
LoadBalancer::execute(Func f, uint32_t ledgerSequence, std::chrono::steady_clock::duration retryAfter)
 | 
			
		||||
{
 | 
			
		||||
    std::size_t sourceIdx = 0;
 | 
			
		||||
    if (!sources_.empty())
 | 
			
		||||
        sourceIdx = util::Random::uniform(0ul, sources_.size() - 1);
 | 
			
		||||
    ASSERT(not sources_.empty(), "ETL sources must be configured to execute functions.");
 | 
			
		||||
    size_t sourceIdx = util::Random::uniform(0ul, sources_.size() - 1);
 | 
			
		||||
 | 
			
		||||
    auto numAttempts = 0;
 | 
			
		||||
    size_t numAttempts = 0;
 | 
			
		||||
 | 
			
		||||
    while (true) {
 | 
			
		||||
        auto& source = sources_[sourceIdx];
 | 
			
		||||
 | 
			
		||||
        LOG(log_.debug()) << "Attempting to execute func. ledger sequence = " << ledgerSequence
 | 
			
		||||
                          << " - source = " << source.toString();
 | 
			
		||||
                          << " - source = " << source->toString();
 | 
			
		||||
        // Originally, it was (source->hasLedger(ledgerSequence) || true)
 | 
			
		||||
        /* Sometimes rippled has ledger but doesn't actually know. However,
 | 
			
		||||
        but this does NOT happen in the normal case and is safe to remove
 | 
			
		||||
        This || true is only needed when loading full history standalone */
 | 
			
		||||
        if (source.hasLedger(ledgerSequence)) {
 | 
			
		||||
        if (source->hasLedger(ledgerSequence)) {
 | 
			
		||||
            bool const res = f(source);
 | 
			
		||||
            if (res) {
 | 
			
		||||
                LOG(log_.debug()) << "Successfully executed func at source = " << source.toString()
 | 
			
		||||
                LOG(log_.debug()) << "Successfully executed func at source = " << source->toString()
 | 
			
		||||
                                  << " - ledger sequence = " << ledgerSequence;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LOG(log_.warn()) << "Failed to execute func at source = " << source.toString()
 | 
			
		||||
            LOG(log_.warn()) << "Failed to execute func at source = " << source->toString()
 | 
			
		||||
                             << " - ledger sequence = " << ledgerSequence;
 | 
			
		||||
        } else {
 | 
			
		||||
            LOG(log_.warn()) << "Ledger not present at source = " << source.toString()
 | 
			
		||||
            LOG(log_.warn()) << "Ledger not present at source = " << source->toString()
 | 
			
		||||
                             << " - ledger sequence = " << ledgerSequence;
 | 
			
		||||
        }
 | 
			
		||||
        sourceIdx = (sourceIdx + 1) % sources_.size();
 | 
			
		||||
@@ -283,10 +291,9 @@ LoadBalancer::execute(Func f, uint32_t ledgerSequence)
 | 
			
		||||
        if (numAttempts % sources_.size() == 0) {
 | 
			
		||||
            LOG(log_.info()) << "Ledger sequence " << ledgerSequence
 | 
			
		||||
                             << " is not yet available from any configured sources. Sleeping and trying again";
 | 
			
		||||
            std::this_thread::sleep_for(std::chrono::seconds(2));
 | 
			
		||||
            std::this_thread::sleep_for(retryAfter);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<ETLState>
 | 
			
		||||
@@ -304,10 +311,11 @@ LoadBalancer::chooseForwardingSource()
 | 
			
		||||
{
 | 
			
		||||
    hasForwardingSource_ = false;
 | 
			
		||||
    for (auto& source : sources_) {
 | 
			
		||||
        if (source.isConnected()) {
 | 
			
		||||
            source.setForwarding(true);
 | 
			
		||||
        if (not hasForwardingSource_ and source->isConnected()) {
 | 
			
		||||
            source->setForwarding(true);
 | 
			
		||||
            hasForwardingSource_ = true;
 | 
			
		||||
            return;
 | 
			
		||||
        } else {
 | 
			
		||||
            source->setForwarding(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
#include "etl/ETLState.hpp"
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "etl/impl/ForwardingCache.hpp"
 | 
			
		||||
#include "feed/SubscriptionManager.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +39,7 @@
 | 
			
		||||
#include <ripple/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
 | 
			
		||||
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
@@ -46,14 +47,6 @@
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
class ProbingSource;
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
 | 
			
		||||
namespace feed {
 | 
			
		||||
class SubscriptionManager;
 | 
			
		||||
}  // namespace feed
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -73,9 +66,9 @@ private:
 | 
			
		||||
    static constexpr std::uint32_t DEFAULT_DOWNLOAD_RANGES = 16;
 | 
			
		||||
 | 
			
		||||
    util::Logger log_{"ETL"};
 | 
			
		||||
    // Forwarding cache must be destroyed after sources because sources have a callnack to invalidate cache
 | 
			
		||||
    // Forwarding cache must be destroyed after sources because sources have a callback to invalidate cache
 | 
			
		||||
    std::optional<impl::ForwardingCache> forwardingCache_;
 | 
			
		||||
    std::vector<Source> sources_;
 | 
			
		||||
    std::vector<SourcePtr> sources_;
 | 
			
		||||
    std::optional<ETLState> etlState_;
 | 
			
		||||
    std::uint32_t downloadRanges_ =
 | 
			
		||||
        DEFAULT_DOWNLOAD_RANGES; /*< The number of markers to use when downloading initial ledger */
 | 
			
		||||
@@ -90,13 +83,15 @@ public:
 | 
			
		||||
     * @param backend BackendInterface implementation
 | 
			
		||||
     * @param subscriptions Subscription manager
 | 
			
		||||
     * @param validatedLedgers The network validated ledgers datastructure
 | 
			
		||||
     * @param sourceFactory A factory function to create a source
 | 
			
		||||
     */
 | 
			
		||||
    LoadBalancer(
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgers> validatedLedgers
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
        SourceFactory sourceFactory = make_Source
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -107,6 +102,7 @@ public:
 | 
			
		||||
     * @param backend BackendInterface implementation
 | 
			
		||||
     * @param subscriptions Subscription manager
 | 
			
		||||
     * @param validatedLedgers The network validated ledgers datastructure
 | 
			
		||||
     * @param sourceFactory A factory function to create a source
 | 
			
		||||
     * @return A shared pointer to a new instance of LoadBalancer
 | 
			
		||||
     */
 | 
			
		||||
    static std::shared_ptr<LoadBalancer>
 | 
			
		||||
@@ -114,22 +110,28 @@ public:
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgers> validatedLedgers
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
        SourceFactory sourceFactory = make_Source
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    ~LoadBalancer();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Load the initial ledger, writing data to the queue.
 | 
			
		||||
     * @note This function will retry indefinitely until the ledger is downloaded.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence Sequence of ledger to download
 | 
			
		||||
     * @param cacheOnly Whether to only write to cache and not to the DB; defaults to false
 | 
			
		||||
     * @return A std::pair<std::vector<std::string>, bool> The ledger data and a bool indicating whether the download
 | 
			
		||||
     * was successful
 | 
			
		||||
     * @param retryAfter Time to wait between retries (2 seconds by default)
 | 
			
		||||
     * @return A std::vector<std::string> The ledger data
 | 
			
		||||
     */
 | 
			
		||||
    std::pair<std::vector<std::string>, bool>
 | 
			
		||||
    loadInitialLedger(uint32_t sequence, bool cacheOnly = false);
 | 
			
		||||
    std::vector<std::string>
 | 
			
		||||
    loadInitialLedger(
 | 
			
		||||
        uint32_t sequence,
 | 
			
		||||
        bool cacheOnly = false,
 | 
			
		||||
        std::chrono::steady_clock::duration retryAfter = std::chrono::seconds{2}
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Fetch data for a specific ledger.
 | 
			
		||||
@@ -140,11 +142,17 @@ public:
 | 
			
		||||
     * @param ledgerSequence Sequence of the ledger to fetch
 | 
			
		||||
     * @param getObjects Whether to get the account state diff between this ledger and the prior one
 | 
			
		||||
     * @param getObjectNeighbors Whether to request object neighbors
 | 
			
		||||
     * @param retryAfter Time to wait between retries (2 seconds by default)
 | 
			
		||||
     * @return The extracted data, if extraction was successful. If the ledger was found
 | 
			
		||||
     * in the database or the server is shutting down, the optional will be empty
 | 
			
		||||
     */
 | 
			
		||||
    OptionalGetLedgerResponseType
 | 
			
		||||
    fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObjectNeighbors);
 | 
			
		||||
    fetchLedger(
 | 
			
		||||
        uint32_t ledgerSequence,
 | 
			
		||||
        bool getObjects,
 | 
			
		||||
        bool getObjectNeighbors,
 | 
			
		||||
        std::chrono::steady_clock::duration retryAfter = std::chrono::seconds{2}
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Represent the state of this load balancer as a JSON object
 | 
			
		||||
@@ -186,12 +194,12 @@ private:
 | 
			
		||||
     *
 | 
			
		||||
     * @param f Function to execute. This function takes the ETL source as an argument, and returns a bool
 | 
			
		||||
     * @param ledgerSequence f is executed for each Source that has this ledger
 | 
			
		||||
     * @return true if f was eventually executed successfully. false if the ledger was found in the database or the
 | 
			
		||||
     * @param retryAfter Time to wait between retries (2 seconds by default)
 | 
			
		||||
     * server is shutting down
 | 
			
		||||
     */
 | 
			
		||||
    template <typename Func>
 | 
			
		||||
    bool
 | 
			
		||||
    execute(Func f, uint32_t ledgerSequence);
 | 
			
		||||
    void
 | 
			
		||||
    execute(Func f, uint32_t ledgerSequence, std::chrono::steady_clock::duration retryAfter = std::chrono::seconds{2});
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Choose a new source to forward requests
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										65
									
								
								src/etl/NetworkValidatedLedgers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/etl/NetworkValidatedLedgers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/NetworkValidatedLedgers.hpp"
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
std::shared_ptr<NetworkValidatedLedgers>
 | 
			
		||||
NetworkValidatedLedgers::make_ValidatedLedgers()
 | 
			
		||||
{
 | 
			
		||||
    return std::make_shared<NetworkValidatedLedgers>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
NetworkValidatedLedgers::push(uint32_t idx)
 | 
			
		||||
{
 | 
			
		||||
    std::lock_guard const lck(m_);
 | 
			
		||||
    if (!max_ || idx > *max_)
 | 
			
		||||
        max_ = idx;
 | 
			
		||||
    cv_.notify_all();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<uint32_t>
 | 
			
		||||
NetworkValidatedLedgers::getMostRecent()
 | 
			
		||||
{
 | 
			
		||||
    std::unique_lock lck(m_);
 | 
			
		||||
    cv_.wait(lck, [this]() { return max_; });
 | 
			
		||||
    return max_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool
 | 
			
		||||
NetworkValidatedLedgers::waitUntilValidatedByNetwork(uint32_t sequence, std::optional<uint32_t> maxWaitMs)
 | 
			
		||||
{
 | 
			
		||||
    std::unique_lock lck(m_);
 | 
			
		||||
    auto pred = [sequence, this]() -> bool { return (max_ && sequence <= *max_); };
 | 
			
		||||
    if (maxWaitMs) {
 | 
			
		||||
        cv_.wait_for(lck, std::chrono::milliseconds(*maxWaitMs));
 | 
			
		||||
    } else {
 | 
			
		||||
        cv_.wait(lck, pred);
 | 
			
		||||
    }
 | 
			
		||||
    return pred();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
							
								
								
									
										86
									
								
								src/etl/NetworkValidatedLedgers.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/etl/NetworkValidatedLedgers.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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 "etl/NetworkValidatedLedgersInterface.hpp"
 | 
			
		||||
 | 
			
		||||
#include <condition_variable>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief This datastructure is used to keep track of the sequence of the most recent ledger validated by the network.
 | 
			
		||||
 *
 | 
			
		||||
 * There are two methods that will wait until certain conditions are met. This datastructure is able to be "stopped".
 | 
			
		||||
 * When the datastructure is stopped, any threads currently waiting are unblocked.
 | 
			
		||||
 * Any later calls to methods of this datastructure will not wait. Once the datastructure is stopped, the datastructure
 | 
			
		||||
 * remains stopped for the rest of its lifetime.
 | 
			
		||||
 */
 | 
			
		||||
class NetworkValidatedLedgers : public NetworkValidatedLedgersInterface {
 | 
			
		||||
    // max sequence validated by network
 | 
			
		||||
    std::optional<uint32_t> max_;
 | 
			
		||||
 | 
			
		||||
    mutable std::mutex m_;
 | 
			
		||||
    std::condition_variable cv_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A factory function for NetworkValidatedLedgers
 | 
			
		||||
     *
 | 
			
		||||
     * @return A shared pointer to a new instance of NetworkValidatedLedgers
 | 
			
		||||
     */
 | 
			
		||||
    static std::shared_ptr<NetworkValidatedLedgers>
 | 
			
		||||
    make_ValidatedLedgers();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Notify the datastructure that idx has been validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param idx Sequence validated by network
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    push(uint32_t idx) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get most recently validated sequence.
 | 
			
		||||
     *
 | 
			
		||||
     * If no ledgers are known to have been validated, this function waits until the next ledger is validated
 | 
			
		||||
     *
 | 
			
		||||
     * @return Sequence of most recently validated ledger. empty optional if the datastructure has been stopped
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<uint32_t>
 | 
			
		||||
    getMostRecent() final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Waits for the sequence to be validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence The sequence to wait for
 | 
			
		||||
     * @param maxWaitMs Maximum time to wait for the sequence to be validated. If empty, wait indefinitely
 | 
			
		||||
     * @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 = {}) final;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
							
								
								
									
										64
									
								
								src/etl/NetworkValidatedLedgersInterface.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/etl/NetworkValidatedLedgersInterface.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
/** @file */
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
namespace etl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief An interface for NetworkValidatedLedgers
 | 
			
		||||
 */
 | 
			
		||||
class NetworkValidatedLedgersInterface {
 | 
			
		||||
public:
 | 
			
		||||
    virtual ~NetworkValidatedLedgersInterface() = default;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Notify the datastructure that idx has been validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param idx Sequence validated by network
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    push(uint32_t idx) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get most recently validated sequence.
 | 
			
		||||
     *
 | 
			
		||||
     * If no ledgers are known to have been validated, this function waits until the next ledger is validated
 | 
			
		||||
     *
 | 
			
		||||
     * @return Sequence of most recently validated ledger. empty optional if the datastructure has been stopped
 | 
			
		||||
     */
 | 
			
		||||
    virtual std::optional<uint32_t>
 | 
			
		||||
    getMostRecent() = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Waits for the sequence to be validated by the network.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence The sequence to wait for
 | 
			
		||||
     * @param maxWaitMs Maximum time to wait for the sequence to be validated. If empty, wait indefinitely
 | 
			
		||||
     * @return true if sequence was validated, false otherwise a return value of false means the datastructure has been
 | 
			
		||||
     * stopped
 | 
			
		||||
     */
 | 
			
		||||
    virtual bool
 | 
			
		||||
    waitUntilValidatedByNetwork(uint32_t sequence, std::optional<uint32_t> maxWaitMs = {}) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
@@ -21,7 +21,11 @@
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManager.hpp"
 | 
			
		||||
#include "etl/impl/ForwardingSource.hpp"
 | 
			
		||||
#include "etl/impl/GrpcSource.hpp"
 | 
			
		||||
#include "etl/impl/SourceImpl.hpp"
 | 
			
		||||
#include "etl/impl/SubscriptionSource.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
@@ -32,18 +36,16 @@
 | 
			
		||||
 | 
			
		||||
namespace etl {
 | 
			
		||||
 | 
			
		||||
template class SourceImpl<>;
 | 
			
		||||
 | 
			
		||||
Source
 | 
			
		||||
SourcePtr
 | 
			
		||||
make_Source(
 | 
			
		||||
    util::Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgers> validatedLedgers,
 | 
			
		||||
    Source::OnDisconnectHook onDisconnect,
 | 
			
		||||
    Source::OnConnectHook onConnect,
 | 
			
		||||
    Source::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    SourceBase::OnConnectHook onConnect,
 | 
			
		||||
    SourceBase::OnDisconnectHook onDisconnect,
 | 
			
		||||
    SourceBase::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    auto const ip = config.valueOr<std::string>("ip", {});
 | 
			
		||||
@@ -63,9 +65,9 @@ make_Source(
 | 
			
		||||
        std::move(onLedgerClosed)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Source{
 | 
			
		||||
    return std::make_unique<impl::SourceImpl<>>(
 | 
			
		||||
        ip, wsPort, grpcPort, std::move(grpcSource), std::move(subscriptionSource), std::move(forwardingSource)
 | 
			
		||||
    };
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "etl/impl/ForwardingSource.hpp"
 | 
			
		||||
#include "etl/impl/GrpcSource.hpp"
 | 
			
		||||
#include "etl/impl/SubscriptionSource.hpp"
 | 
			
		||||
#include "feed/SubscriptionManager.hpp"
 | 
			
		||||
#include "etl/NetworkValidatedLedgersInterface.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
@@ -35,8 +32,8 @@
 | 
			
		||||
#include <grpcpp/support/status.h>
 | 
			
		||||
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
@@ -48,122 +45,48 @@ namespace etl {
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Provides an implementation of a ETL source
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam GrpcSourceType The type of the gRPC source
 | 
			
		||||
 * @tparam SubscriptionSourceTypePtr The type of the subscription source
 | 
			
		||||
 * @tparam ForwardingSourceType The type of the forwarding source
 | 
			
		||||
 */
 | 
			
		||||
template <
 | 
			
		||||
    typename GrpcSourceType = impl::GrpcSource,
 | 
			
		||||
    typename SubscriptionSourceTypePtr = std::unique_ptr<impl::SubscriptionSource>,
 | 
			
		||||
    typename ForwardingSourceType = impl::ForwardingSource>
 | 
			
		||||
class SourceImpl {
 | 
			
		||||
    std::string ip_;
 | 
			
		||||
    std::string wsPort_;
 | 
			
		||||
    std::string grpcPort_;
 | 
			
		||||
 | 
			
		||||
    GrpcSourceType grpcSource_;
 | 
			
		||||
    SubscriptionSourceTypePtr subscriptionSource_;
 | 
			
		||||
    ForwardingSourceType forwardingSource_;
 | 
			
		||||
 | 
			
		||||
class SourceBase {
 | 
			
		||||
public:
 | 
			
		||||
    using OnConnectHook = impl::SubscriptionSource::OnConnectHook;
 | 
			
		||||
    using OnDisconnectHook = impl::SubscriptionSource::OnDisconnectHook;
 | 
			
		||||
    using OnLedgerClosedHook = impl::SubscriptionSource::OnLedgerClosedHook;
 | 
			
		||||
    using OnConnectHook = std::function<void()>;
 | 
			
		||||
    using OnDisconnectHook = std::function<void()>;
 | 
			
		||||
    using OnLedgerClosedHook = std::function<void()>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new SourceImpl object
 | 
			
		||||
     *
 | 
			
		||||
     * @param ip The IP of the source
 | 
			
		||||
     * @param wsPort The web socket port of the source
 | 
			
		||||
     * @param grpcPort The gRPC port of the source
 | 
			
		||||
     * @param grpcSource The gRPC source
 | 
			
		||||
     * @param subscriptionSource The subscription source
 | 
			
		||||
     * @param forwardingSource The forwarding source
 | 
			
		||||
     */
 | 
			
		||||
    template <typename SomeGrpcSourceType, typename SomeForwardingSourceType>
 | 
			
		||||
        requires std::is_same_v<GrpcSourceType, SomeGrpcSourceType> and
 | 
			
		||||
                     std::is_same_v<ForwardingSourceType, SomeForwardingSourceType>
 | 
			
		||||
    SourceImpl(
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::string wsPort,
 | 
			
		||||
        std::string grpcPort,
 | 
			
		||||
        SomeGrpcSourceType&& grpcSource,
 | 
			
		||||
        SubscriptionSourceTypePtr subscriptionSource,
 | 
			
		||||
        SomeForwardingSourceType&& forwardingSource
 | 
			
		||||
    )
 | 
			
		||||
        : ip_(std::move(ip))
 | 
			
		||||
        , wsPort_(std::move(wsPort))
 | 
			
		||||
        , grpcPort_(std::move(grpcPort))
 | 
			
		||||
        , grpcSource_(std::forward<SomeGrpcSourceType>(grpcSource))
 | 
			
		||||
        , subscriptionSource_(std::move(subscriptionSource))
 | 
			
		||||
        , forwardingSource_(std::forward<SomeForwardingSourceType>(forwardingSource))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    virtual ~SourceBase() = default;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Run subscriptions loop of the source
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        subscriptionSource_->run();
 | 
			
		||||
    }
 | 
			
		||||
    virtual void
 | 
			
		||||
    run() = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check if source is connected
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if source is connected; false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    isConnected() const
 | 
			
		||||
    {
 | 
			
		||||
        return subscriptionSource_->isConnected();
 | 
			
		||||
    }
 | 
			
		||||
    virtual bool
 | 
			
		||||
    isConnected() const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the forwarding state of the source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isForwarding Whether to forward or not
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    setForwarding(bool isForwarding)
 | 
			
		||||
    {
 | 
			
		||||
        subscriptionSource_->setForwarding(isForwarding);
 | 
			
		||||
    }
 | 
			
		||||
    virtual void
 | 
			
		||||
    setForwarding(bool isForwarding) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Represent the source as a JSON object
 | 
			
		||||
     *
 | 
			
		||||
     * @return JSON representation of the source
 | 
			
		||||
     */
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    toJson() const
 | 
			
		||||
    {
 | 
			
		||||
        boost::json::object res;
 | 
			
		||||
 | 
			
		||||
        res["validated_range"] = subscriptionSource_->validatedRange();
 | 
			
		||||
        res["is_connected"] = std::to_string(static_cast<int>(subscriptionSource_->isConnected()));
 | 
			
		||||
        res["ip"] = ip_;
 | 
			
		||||
        res["ws_port"] = wsPort_;
 | 
			
		||||
        res["grpc_port"] = grpcPort_;
 | 
			
		||||
 | 
			
		||||
        auto last = subscriptionSource_->lastMessageTime();
 | 
			
		||||
        if (last.time_since_epoch().count() != 0) {
 | 
			
		||||
            res["last_msg_age_seconds"] = std::to_string(
 | 
			
		||||
                std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - last).count()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
    virtual boost::json::object
 | 
			
		||||
    toJson() const = 0;
 | 
			
		||||
 | 
			
		||||
    /** @return String representation of the source (for debug) */
 | 
			
		||||
    std::string
 | 
			
		||||
    toString() const
 | 
			
		||||
    {
 | 
			
		||||
        return "{validated range: " + subscriptionSource_->validatedRange() + ", ip: " + ip_ +
 | 
			
		||||
            ", web socket port: " + wsPort_ + ", grpc port: " + grpcPort_ + "}";
 | 
			
		||||
    }
 | 
			
		||||
    virtual std::string
 | 
			
		||||
    toString() const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check if ledger is known by this source.
 | 
			
		||||
@@ -171,11 +94,8 @@ public:
 | 
			
		||||
     * @param sequence The ledger sequence to check
 | 
			
		||||
     * @return true if ledger is in the range of this source; false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    hasLedger(uint32_t sequence) const
 | 
			
		||||
    {
 | 
			
		||||
        return subscriptionSource_->hasLedger(sequence);
 | 
			
		||||
    }
 | 
			
		||||
    virtual bool
 | 
			
		||||
    hasLedger(uint32_t sequence) const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Fetch data for a specific ledger.
 | 
			
		||||
@@ -188,11 +108,8 @@ public:
 | 
			
		||||
     * @param getObjectNeighbors Whether to request object neighbors; defaults to false
 | 
			
		||||
     * @return A std::pair of the response status and the response itself
 | 
			
		||||
     */
 | 
			
		||||
    std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
 | 
			
		||||
    fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false)
 | 
			
		||||
    {
 | 
			
		||||
        return grpcSource_.fetchLedger(sequence, getObjects, getObjectNeighbors);
 | 
			
		||||
    }
 | 
			
		||||
    virtual std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
 | 
			
		||||
    fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Download a ledger in full.
 | 
			
		||||
@@ -202,11 +119,8 @@ public:
 | 
			
		||||
     * @param cacheOnly Only insert into cache, not the DB; defaults to false
 | 
			
		||||
     * @return A std::pair of the data and a bool indicating whether the download was successful
 | 
			
		||||
     */
 | 
			
		||||
    std::pair<std::vector<std::string>, bool>
 | 
			
		||||
    loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false)
 | 
			
		||||
    {
 | 
			
		||||
        return grpcSource_.loadInitialLedger(sequence, numMarkers, cacheOnly);
 | 
			
		||||
    }
 | 
			
		||||
    virtual std::pair<std::vector<std::string>, bool>
 | 
			
		||||
    loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward a request to rippled.
 | 
			
		||||
@@ -216,20 +130,26 @@ public:
 | 
			
		||||
     * @param yield The coroutine context
 | 
			
		||||
     * @return Response wrapped in an optional on success; nullopt otherwise
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<boost::json::object>
 | 
			
		||||
    virtual std::optional<boost::json::object>
 | 
			
		||||
    forwardToRippled(
 | 
			
		||||
        boost::json::object const& request,
 | 
			
		||||
        std::optional<std::string> const& forwardToRippledClientIp,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    ) const
 | 
			
		||||
    {
 | 
			
		||||
        return forwardingSource_.forwardToRippled(request, forwardToRippledClientIp, yield);
 | 
			
		||||
    }
 | 
			
		||||
    ) const = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern template class SourceImpl<>;
 | 
			
		||||
using SourcePtr = std::unique_ptr<SourceBase>;
 | 
			
		||||
 | 
			
		||||
using Source = SourceImpl<>;
 | 
			
		||||
using SourceFactory = std::function<SourcePtr(
 | 
			
		||||
    util::Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    SourceBase::OnConnectHook onConnect,
 | 
			
		||||
    SourceBase::OnDisconnectHook onDisconnect,
 | 
			
		||||
    SourceBase::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
)>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Create a source
 | 
			
		||||
@@ -239,22 +159,22 @@ using Source = SourceImpl<>;
 | 
			
		||||
 * @param backend BackendInterface implementation
 | 
			
		||||
 * @param subscriptions Subscription manager
 | 
			
		||||
 * @param validatedLedgers The network validated ledgers data structure
 | 
			
		||||
 * @param onDisconnect The hook to call on disconnect
 | 
			
		||||
 * @param onConnect The hook to call on connect
 | 
			
		||||
 * @param onDisconnect The hook to call on disconnect
 | 
			
		||||
 * @param onLedgerClosed The hook to call on ledger closed. This is called when a ledger is closed and the source is set
 | 
			
		||||
 * as forwarding.
 | 
			
		||||
 * @return The created source
 | 
			
		||||
 */
 | 
			
		||||
Source
 | 
			
		||||
SourcePtr
 | 
			
		||||
make_Source(
 | 
			
		||||
    util::Config const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgers> validatedLedgers,
 | 
			
		||||
    Source::OnDisconnectHook onDisconnect,
 | 
			
		||||
    Source::OnConnectHook onConnect,
 | 
			
		||||
    Source::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    SourceBase::OnConnectHook onConnect,
 | 
			
		||||
    SourceBase::OnDisconnectHook onDisconnect,
 | 
			
		||||
    SourceBase::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
 
 | 
			
		||||
@@ -99,7 +99,8 @@ public:
 | 
			
		||||
        bool cacheOnly = false
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        LOG(log_.trace()) << "Processing response. " << "Marker prefix = " << getMarkerPrefix();
 | 
			
		||||
        LOG(log_.trace()) << "Processing response. "
 | 
			
		||||
                          << "Marker prefix = " << getMarkerPrefix();
 | 
			
		||||
        if (abort) {
 | 
			
		||||
            LOG(log_.error()) << "AsyncCallData aborted";
 | 
			
		||||
            return CallStatus::ERRORED;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "etl/NetworkValidatedLedgersInterface.hpp"
 | 
			
		||||
#include "etl/SystemState.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Profiler.hpp"
 | 
			
		||||
@@ -39,12 +40,12 @@ namespace etl::impl {
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Extractor thread that is fetching GRPC data and enqueue it on the DataPipeType
 | 
			
		||||
 */
 | 
			
		||||
template <typename DataPipeType, typename NetworkValidatedLedgersType, typename LedgerFetcherType>
 | 
			
		||||
template <typename DataPipeType, typename LedgerFetcherType>
 | 
			
		||||
class Extractor {
 | 
			
		||||
    util::Logger log_{"ETL"};
 | 
			
		||||
 | 
			
		||||
    std::reference_wrapper<DataPipeType> pipe_;
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersType> networkValidatedLedgers_;
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> networkValidatedLedgers_;
 | 
			
		||||
    std::reference_wrapper<LedgerFetcherType> ledgerFetcher_;
 | 
			
		||||
    uint32_t startSequence_;
 | 
			
		||||
    std::optional<uint32_t> finishSequence_;
 | 
			
		||||
@@ -55,7 +56,7 @@ class Extractor {
 | 
			
		||||
public:
 | 
			
		||||
    Extractor(
 | 
			
		||||
        DataPipeType& pipe,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersType> networkValidatedLedgers,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> networkValidatedLedgers,
 | 
			
		||||
        LedgerFetcherType& ledgerFetcher,
 | 
			
		||||
        uint32_t startSequence,
 | 
			
		||||
        std::optional<uint32_t> finishSequence,
 | 
			
		||||
 
 | 
			
		||||
@@ -197,58 +197,55 @@ public:
 | 
			
		||||
            // asyncWriter consumes from the queue and inserts the data into the
 | 
			
		||||
            // Ledger object. Once the below call returns, all data has been pushed
 | 
			
		||||
            // into the queue
 | 
			
		||||
            auto [edgeKeys, success] = loadBalancer_->loadInitialLedger(sequence);
 | 
			
		||||
            auto edgeKeys = loadBalancer_->loadInitialLedger(sequence);
 | 
			
		||||
 | 
			
		||||
            if (success) {
 | 
			
		||||
                size_t numWrites = 0;
 | 
			
		||||
                backend_->cache().setFull();
 | 
			
		||||
            size_t numWrites = 0;
 | 
			
		||||
            backend_->cache().setFull();
 | 
			
		||||
 | 
			
		||||
                auto seconds = ::util::timed<std::chrono::seconds>([this, keys = &edgeKeys, sequence, &numWrites] {
 | 
			
		||||
                    for (auto& key : *keys) {
 | 
			
		||||
                        LOG(log_.debug()) << "Writing edge key = " << ripple::strHex(key);
 | 
			
		||||
                        auto succ = backend_->cache().getSuccessor(*ripple::uint256::fromVoidChecked(key), sequence);
 | 
			
		||||
                        if (succ)
 | 
			
		||||
                            backend_->writeSuccessor(std::move(key), sequence, uint256ToString(succ->key));
 | 
			
		||||
                    }
 | 
			
		||||
            auto seconds = ::util::timed<std::chrono::seconds>([this, keys = std::move(edgeKeys), sequence, &numWrites](
 | 
			
		||||
                                                               ) mutable {
 | 
			
		||||
                for (auto& key : keys) {
 | 
			
		||||
                    LOG(log_.debug()) << "Writing edge key = " << ripple::strHex(key);
 | 
			
		||||
                    auto succ = backend_->cache().getSuccessor(*ripple::uint256::fromVoidChecked(key), sequence);
 | 
			
		||||
                    if (succ)
 | 
			
		||||
                        backend_->writeSuccessor(std::move(key), sequence, uint256ToString(succ->key));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    ripple::uint256 prev = data::firstKey;
 | 
			
		||||
                    while (auto cur = backend_->cache().getSuccessor(prev, sequence)) {
 | 
			
		||||
                        ASSERT(cur.has_value(), "Succesor for key {} must exist", ripple::strHex(prev));
 | 
			
		||||
                        if (prev == data::firstKey)
 | 
			
		||||
                            backend_->writeSuccessor(uint256ToString(prev), sequence, uint256ToString(cur->key));
 | 
			
		||||
                ripple::uint256 prev = data::firstKey;
 | 
			
		||||
                while (auto cur = backend_->cache().getSuccessor(prev, sequence)) {
 | 
			
		||||
                    ASSERT(cur.has_value(), "Succesor for key {} must exist", ripple::strHex(prev));
 | 
			
		||||
                    if (prev == data::firstKey)
 | 
			
		||||
                        backend_->writeSuccessor(uint256ToString(prev), sequence, uint256ToString(cur->key));
 | 
			
		||||
 | 
			
		||||
                        if (isBookDir(cur->key, cur->blob)) {
 | 
			
		||||
                            auto base = getBookBase(cur->key);
 | 
			
		||||
                            // make sure the base is not an actual object
 | 
			
		||||
                            if (!backend_->cache().get(base, sequence)) {
 | 
			
		||||
                                auto succ = backend_->cache().getSuccessor(base, sequence);
 | 
			
		||||
                                ASSERT(succ.has_value(), "Book base {} must have a successor", ripple::strHex(base));
 | 
			
		||||
                                if (succ->key == cur->key) {
 | 
			
		||||
                                    LOG(log_.debug()) << "Writing book successor = " << ripple::strHex(base) << " - "
 | 
			
		||||
                                                      << ripple::strHex(cur->key);
 | 
			
		||||
                    if (isBookDir(cur->key, cur->blob)) {
 | 
			
		||||
                        auto base = getBookBase(cur->key);
 | 
			
		||||
                        // make sure the base is not an actual object
 | 
			
		||||
                        if (!backend_->cache().get(base, sequence)) {
 | 
			
		||||
                            auto succ = backend_->cache().getSuccessor(base, sequence);
 | 
			
		||||
                            ASSERT(succ.has_value(), "Book base {} must have a successor", ripple::strHex(base));
 | 
			
		||||
                            if (succ->key == cur->key) {
 | 
			
		||||
                                LOG(log_.debug()) << "Writing book successor = " << ripple::strHex(base) << " - "
 | 
			
		||||
                                                  << ripple::strHex(cur->key);
 | 
			
		||||
 | 
			
		||||
                                    backend_->writeSuccessor(
 | 
			
		||||
                                        uint256ToString(base), sequence, uint256ToString(cur->key)
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
                                backend_->writeSuccessor(uint256ToString(base), sequence, uint256ToString(cur->key));
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            ++numWrites;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        prev = cur->key;
 | 
			
		||||
                        static constexpr std::size_t LOG_INTERVAL = 100000;
 | 
			
		||||
                        if (numWrites % LOG_INTERVAL == 0 && numWrites != 0)
 | 
			
		||||
                            LOG(log_.info()) << "Wrote " << numWrites << " book successors";
 | 
			
		||||
                        ++numWrites;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    backend_->writeSuccessor(uint256ToString(prev), sequence, uint256ToString(data::lastKey));
 | 
			
		||||
                    ++numWrites;
 | 
			
		||||
                });
 | 
			
		||||
                    prev = cur->key;
 | 
			
		||||
                    static constexpr std::size_t LOG_INTERVAL = 100000;
 | 
			
		||||
                    if (numWrites % LOG_INTERVAL == 0 && numWrites != 0)
 | 
			
		||||
                        LOG(log_.info()) << "Wrote " << numWrites << " book successors";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                LOG(log_.info()) << "Looping through cache and submitting all writes took " << seconds
 | 
			
		||||
                                 << " seconds. numWrites = " << std::to_string(numWrites);
 | 
			
		||||
            }
 | 
			
		||||
                backend_->writeSuccessor(uint256ToString(prev), sequence, uint256ToString(data::lastKey));
 | 
			
		||||
                ++numWrites;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            LOG(log_.info()) << "Looping through cache and submitting all writes took " << seconds
 | 
			
		||||
                             << " seconds. numWrites = " << std::to_string(numWrites);
 | 
			
		||||
 | 
			
		||||
            LOG(log_.debug()) << "Loaded initial ledger";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@
 | 
			
		||||
#include "data/DBHelpers.hpp"
 | 
			
		||||
#include "data/Types.hpp"
 | 
			
		||||
#include "etl/SystemState.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "util/prometheus/Counter.hpp"
 | 
			
		||||
@@ -63,7 +64,7 @@ namespace etl::impl {
 | 
			
		||||
 * includes reading all of the transactions from the database) is done from the application wide asio io_service, and a
 | 
			
		||||
 * strand is used to ensure ledgers are published in order.
 | 
			
		||||
 */
 | 
			
		||||
template <typename SubscriptionManagerType, typename CacheType>
 | 
			
		||||
template <typename CacheType>
 | 
			
		||||
class LedgerPublisher {
 | 
			
		||||
    util::Logger log_{"ETL"};
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +72,7 @@ class LedgerPublisher {
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend_;
 | 
			
		||||
    std::reference_wrapper<CacheType> cache_;
 | 
			
		||||
    std::shared_ptr<SubscriptionManagerType> subscriptions_;
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
 | 
			
		||||
    std::reference_wrapper<SystemState const> state_;  // shared state for ETL
 | 
			
		||||
 | 
			
		||||
    std::chrono::time_point<ripple::NetClock> lastCloseTime_;
 | 
			
		||||
@@ -94,7 +95,7 @@ public:
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        CacheType& cache,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> subscriptions,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        SystemState const& state
 | 
			
		||||
    )
 | 
			
		||||
        : publishStrand_{boost::asio::make_strand(ioc)}
 | 
			
		||||
@@ -110,11 +111,16 @@ public:
 | 
			
		||||
     * stream.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ledgerSequence the sequence of the ledger to publish
 | 
			
		||||
     * @param maxAttempts the number of times to attempt to read the ledger from the database. 1 attempt per second
 | 
			
		||||
     * @param maxAttempts the number of times to attempt to read the ledger from the database
 | 
			
		||||
     * @param attemptsDelay the delay between attempts to read the ledger from the database
 | 
			
		||||
     * @return Whether the ledger was found in the database and published
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    publish(uint32_t ledgerSequence, std::optional<uint32_t> maxAttempts)
 | 
			
		||||
    publish(
 | 
			
		||||
        uint32_t ledgerSequence,
 | 
			
		||||
        std::optional<uint32_t> maxAttempts,
 | 
			
		||||
        std::chrono::steady_clock::duration attemptsDelay = std::chrono::seconds{1}
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        LOG(log_.info()) << "Attempting to publish ledger = " << ledgerSequence;
 | 
			
		||||
        size_t numAttempts = 0;
 | 
			
		||||
@@ -130,7 +136,7 @@ public:
 | 
			
		||||
                    LOG(log_.debug()) << "Failed to publish ledger after " << numAttempts << " attempts.";
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                std::this_thread::sleep_for(std::chrono::seconds(1));
 | 
			
		||||
                std::this_thread::sleep_for(attemptsDelay);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										219
									
								
								src/etl/impl/SourceImpl.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/etl/impl/SourceImpl.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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 "etl/Source.hpp"
 | 
			
		||||
#include "etl/impl/ForwardingSource.hpp"
 | 
			
		||||
#include "etl/impl/GrpcSource.hpp"
 | 
			
		||||
#include "etl/impl/SubscriptionSource.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <grpcpp/support/status.h>
 | 
			
		||||
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
namespace etl::impl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Provides an implementation of a ETL source
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam GrpcSourceType The type of the gRPC source
 | 
			
		||||
 * @tparam SubscriptionSourceTypePtr The type of the subscription source
 | 
			
		||||
 * @tparam ForwardingSourceType The type of the forwarding source
 | 
			
		||||
 */
 | 
			
		||||
template <
 | 
			
		||||
    typename GrpcSourceType = GrpcSource,
 | 
			
		||||
    typename SubscriptionSourceTypePtr = std::unique_ptr<SubscriptionSource>,
 | 
			
		||||
    typename ForwardingSourceType = ForwardingSource>
 | 
			
		||||
class SourceImpl : public SourceBase {
 | 
			
		||||
    std::string ip_;
 | 
			
		||||
    std::string wsPort_;
 | 
			
		||||
    std::string grpcPort_;
 | 
			
		||||
 | 
			
		||||
    GrpcSourceType grpcSource_;
 | 
			
		||||
    SubscriptionSourceTypePtr subscriptionSource_;
 | 
			
		||||
    ForwardingSourceType forwardingSource_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new SourceImpl object
 | 
			
		||||
     *
 | 
			
		||||
     * @param ip The IP of the source
 | 
			
		||||
     * @param wsPort The web socket port of the source
 | 
			
		||||
     * @param grpcPort The gRPC port of the source
 | 
			
		||||
     * @param grpcSource The gRPC source
 | 
			
		||||
     * @param subscriptionSource The subscription source
 | 
			
		||||
     * @param forwardingSource The forwarding source
 | 
			
		||||
     */
 | 
			
		||||
    template <typename SomeGrpcSourceType, typename SomeForwardingSourceType>
 | 
			
		||||
        requires std::is_same_v<GrpcSourceType, SomeGrpcSourceType> and
 | 
			
		||||
                     std::is_same_v<ForwardingSourceType, SomeForwardingSourceType>
 | 
			
		||||
    SourceImpl(
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::string wsPort,
 | 
			
		||||
        std::string grpcPort,
 | 
			
		||||
        SomeGrpcSourceType&& grpcSource,
 | 
			
		||||
        SubscriptionSourceTypePtr subscriptionSource,
 | 
			
		||||
        SomeForwardingSourceType&& forwardingSource
 | 
			
		||||
    )
 | 
			
		||||
        : ip_(std::move(ip))
 | 
			
		||||
        , wsPort_(std::move(wsPort))
 | 
			
		||||
        , grpcPort_(std::move(grpcPort))
 | 
			
		||||
        , grpcSource_(std::forward<SomeGrpcSourceType>(grpcSource))
 | 
			
		||||
        , subscriptionSource_(std::move(subscriptionSource))
 | 
			
		||||
        , forwardingSource_(std::forward<SomeForwardingSourceType>(forwardingSource))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Run subscriptions loop of the source
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    run() final
 | 
			
		||||
    {
 | 
			
		||||
        subscriptionSource_->run();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check if source is connected
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if source is connected; false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    isConnected() const final
 | 
			
		||||
    {
 | 
			
		||||
        return subscriptionSource_->isConnected();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the forwarding state of the source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isForwarding Whether to forward or not
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    setForwarding(bool isForwarding) final
 | 
			
		||||
    {
 | 
			
		||||
        subscriptionSource_->setForwarding(isForwarding);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Represent the source as a JSON object
 | 
			
		||||
     *
 | 
			
		||||
     * @return JSON representation of the source
 | 
			
		||||
     */
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    toJson() const final
 | 
			
		||||
    {
 | 
			
		||||
        boost::json::object res;
 | 
			
		||||
 | 
			
		||||
        res["validated_range"] = subscriptionSource_->validatedRange();
 | 
			
		||||
        res["is_connected"] = std::to_string(static_cast<int>(subscriptionSource_->isConnected()));
 | 
			
		||||
        res["ip"] = ip_;
 | 
			
		||||
        res["ws_port"] = wsPort_;
 | 
			
		||||
        res["grpc_port"] = grpcPort_;
 | 
			
		||||
 | 
			
		||||
        auto last = subscriptionSource_->lastMessageTime();
 | 
			
		||||
        if (last.time_since_epoch().count() != 0) {
 | 
			
		||||
            res["last_msg_age_seconds"] = std::to_string(
 | 
			
		||||
                std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - last).count()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return String representation of the source (for debug) */
 | 
			
		||||
    std::string
 | 
			
		||||
    toString() const final
 | 
			
		||||
    {
 | 
			
		||||
        return "{validated range: " + subscriptionSource_->validatedRange() + ", ip: " + ip_ +
 | 
			
		||||
            ", web socket port: " + wsPort_ + ", grpc port: " + grpcPort_ + "}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check if ledger is known by this source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence The ledger sequence to check
 | 
			
		||||
     * @return true if ledger is in the range of this source; false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    hasLedger(uint32_t sequence) const final
 | 
			
		||||
    {
 | 
			
		||||
        return subscriptionSource_->hasLedger(sequence);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Fetch data for a specific ledger.
 | 
			
		||||
     *
 | 
			
		||||
     * This function will continuously try to fetch data for the specified ledger until the fetch succeeds, the ledger
 | 
			
		||||
     * is found in the database, or the server is shutting down.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence Sequence of the ledger to fetch
 | 
			
		||||
     * @param getObjects Whether to get the account state diff between this ledger and the prior one; defaults to true
 | 
			
		||||
     * @param getObjectNeighbors Whether to request object neighbors; defaults to false
 | 
			
		||||
     * @return A std::pair of the response status and the response itself
 | 
			
		||||
     */
 | 
			
		||||
    std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
 | 
			
		||||
    fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) final
 | 
			
		||||
    {
 | 
			
		||||
        return grpcSource_.fetchLedger(sequence, getObjects, getObjectNeighbors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Download a ledger in full.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sequence Sequence of the ledger to download
 | 
			
		||||
     * @param numMarkers Number of markers to generate for async calls
 | 
			
		||||
     * @param cacheOnly Only insert into cache, not the DB; defaults to false
 | 
			
		||||
     * @return A std::pair of the data and a bool indicating whether the download was successful
 | 
			
		||||
     */
 | 
			
		||||
    std::pair<std::vector<std::string>, bool>
 | 
			
		||||
    loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) final
 | 
			
		||||
    {
 | 
			
		||||
        return grpcSource_.loadInitialLedger(sequence, numMarkers, cacheOnly);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward a request to rippled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param request The request to forward
 | 
			
		||||
     * @param forwardToRippledClientIp IP of the client forwarding this request if known
 | 
			
		||||
     * @param yield The coroutine context
 | 
			
		||||
     * @return Response wrapped in an optional on success; nullopt otherwise
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<boost::json::object>
 | 
			
		||||
    forwardToRippled(
 | 
			
		||||
        boost::json::object const& request,
 | 
			
		||||
        std::optional<std::string> const& forwardToRippledClientIp,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    ) const final
 | 
			
		||||
    {
 | 
			
		||||
        return forwardingSource_.forwardToRippled(request, forwardToRippledClientIp, yield);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace etl::impl
 | 
			
		||||
@@ -19,6 +19,8 @@
 | 
			
		||||
 | 
			
		||||
#include "etl/impl/SubscriptionSource.hpp"
 | 
			
		||||
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "util/Retry.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
@@ -28,8 +30,11 @@
 | 
			
		||||
#include <boost/algorithm/string/split.hpp>
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/strand.hpp>
 | 
			
		||||
#include <boost/asio/use_future.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
@@ -52,6 +57,33 @@
 | 
			
		||||
 | 
			
		||||
namespace etl::impl {
 | 
			
		||||
 | 
			
		||||
SubscriptionSource::SubscriptionSource(
 | 
			
		||||
    boost::asio::io_context& ioContext,
 | 
			
		||||
    std::string const& ip,
 | 
			
		||||
    std::string const& wsPort,
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
    OnConnectHook onConnect,
 | 
			
		||||
    OnDisconnectHook onDisconnect,
 | 
			
		||||
    OnLedgerClosedHook onLedgerClosed,
 | 
			
		||||
    std::chrono::steady_clock::duration const connectionTimeout,
 | 
			
		||||
    std::chrono::steady_clock::duration const retryDelay
 | 
			
		||||
)
 | 
			
		||||
    : log_(fmt::format("GrpcSource[{}:{}]", ip, wsPort))
 | 
			
		||||
    , wsConnectionBuilder_(ip, wsPort)
 | 
			
		||||
    , validatedLedgers_(std::move(validatedLedgers))
 | 
			
		||||
    , subscriptions_(std::move(subscriptions))
 | 
			
		||||
    , strand_(boost::asio::make_strand(ioContext))
 | 
			
		||||
    , retry_(util::makeRetryExponentialBackoff(retryDelay, RETRY_MAX_DELAY, strand_))
 | 
			
		||||
    , onConnect_(std::move(onConnect))
 | 
			
		||||
    , onDisconnect_(std::move(onDisconnect))
 | 
			
		||||
    , onLedgerClosed_(std::move(onLedgerClosed))
 | 
			
		||||
{
 | 
			
		||||
    wsConnectionBuilder_.addHeader({boost::beast::http::field::user_agent, "clio-client"})
 | 
			
		||||
        .addHeader({"X-User", "clio-client"})
 | 
			
		||||
        .setConnectionTimeout(connectionTimeout);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SubscriptionSource::~SubscriptionSource()
 | 
			
		||||
{
 | 
			
		||||
    stop();
 | 
			
		||||
@@ -91,6 +123,12 @@ SubscriptionSource::isConnected() const
 | 
			
		||||
    return isConnected_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool
 | 
			
		||||
SubscriptionSource::isForwarding() const
 | 
			
		||||
{
 | 
			
		||||
    return isForwarding_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionSource::setForwarding(bool isForwarding)
 | 
			
		||||
{
 | 
			
		||||
@@ -203,18 +241,18 @@ SubscriptionSource::handleMessage(std::string const& message)
 | 
			
		||||
        } else {
 | 
			
		||||
            if (isForwarding_) {
 | 
			
		||||
                if (object.contains(JS(transaction))) {
 | 
			
		||||
                    dependencies_.forwardProposedTransaction(object);
 | 
			
		||||
                    subscriptions_->forwardProposedTransaction(object);
 | 
			
		||||
                } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ValidationReceived) {
 | 
			
		||||
                    dependencies_.forwardValidation(object);
 | 
			
		||||
                    subscriptions_->forwardValidation(object);
 | 
			
		||||
                } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ManifestReceived) {
 | 
			
		||||
                    dependencies_.forwardManifest(object);
 | 
			
		||||
                    subscriptions_->forwardManifest(object);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (ledgerIndex != 0) {
 | 
			
		||||
            LOG(log_.trace()) << "Pushing ledger sequence = " << ledgerIndex;
 | 
			
		||||
            dependencies_.pushValidatedLedger(ledgerIndex);
 | 
			
		||||
            validatedLedgers_->push(ledgerIndex);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
@@ -228,9 +266,9 @@ void
 | 
			
		||||
SubscriptionSource::handleError(util::requests::RequestError const& error, boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    isConnected_ = false;
 | 
			
		||||
    isForwarding_ = false;
 | 
			
		||||
    if (not stop_) {
 | 
			
		||||
        onDisconnect_();
 | 
			
		||||
        isForwarding_ = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (wsConnection_ != nullptr) {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,9 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "etl/impl/SubscriptionSourceDependencies.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/Mutex.hpp"
 | 
			
		||||
#include "util/Retry.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
@@ -35,7 +37,6 @@
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <future>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
@@ -50,9 +51,9 @@ namespace etl::impl {
 | 
			
		||||
 */
 | 
			
		||||
class SubscriptionSource {
 | 
			
		||||
public:
 | 
			
		||||
    using OnConnectHook = std::function<void()>;
 | 
			
		||||
    using OnDisconnectHook = std::function<void()>;
 | 
			
		||||
    using OnLedgerClosedHook = std::function<void()>;
 | 
			
		||||
    using OnConnectHook = SourceBase::OnConnectHook;
 | 
			
		||||
    using OnDisconnectHook = SourceBase::OnDisconnectHook;
 | 
			
		||||
    using OnLedgerClosedHook = SourceBase::OnLedgerClosedHook;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    util::Logger log_;
 | 
			
		||||
@@ -65,7 +66,8 @@ private:
 | 
			
		||||
    };
 | 
			
		||||
    util::Mutex<ValidatedLedgersData> validatedLedgersData_;
 | 
			
		||||
 | 
			
		||||
    SubscriptionSourceDependencies dependencies_;
 | 
			
		||||
    std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers_;
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
 | 
			
		||||
 | 
			
		||||
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +94,6 @@ public:
 | 
			
		||||
     * @brief Construct a new Subscription Source object
 | 
			
		||||
     *
 | 
			
		||||
     * @tparam NetworkValidatedLedgersType The type of the network validated ledgers object
 | 
			
		||||
     * @tparam SubscriptionManagerType The type of the subscription manager object
 | 
			
		||||
     * @param ioContext The io_context to use
 | 
			
		||||
     * @param ip The ip address of the source
 | 
			
		||||
     * @param wsPort The port of the source
 | 
			
		||||
@@ -105,32 +106,18 @@ public:
 | 
			
		||||
     * @param connectionTimeout The connection timeout. Defaults to 30 seconds
 | 
			
		||||
     * @param retryDelay The retry delay. Defaults to 1 second
 | 
			
		||||
     */
 | 
			
		||||
    template <typename NetworkValidatedLedgersType, typename SubscriptionManagerType>
 | 
			
		||||
    SubscriptionSource(
 | 
			
		||||
        boost::asio::io_context& ioContext,
 | 
			
		||||
        std::string const& ip,
 | 
			
		||||
        std::string const& wsPort,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersType> validatedLedgers,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersInterface> validatedLedgers,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions,
 | 
			
		||||
        OnConnectHook onConnect,
 | 
			
		||||
        OnDisconnectHook onDisconnect,
 | 
			
		||||
        OnLedgerClosedHook onLedgerClosed,
 | 
			
		||||
        std::chrono::steady_clock::duration const connectionTimeout = CONNECTION_TIMEOUT,
 | 
			
		||||
        std::chrono::steady_clock::duration const retryDelay = RETRY_DELAY
 | 
			
		||||
    )
 | 
			
		||||
        : log_(fmt::format("GrpcSource[{}:{}]", ip, wsPort))
 | 
			
		||||
        , wsConnectionBuilder_(ip, wsPort)
 | 
			
		||||
        , dependencies_(std::move(validatedLedgers), std::move(subscriptions))
 | 
			
		||||
        , strand_(boost::asio::make_strand(ioContext))
 | 
			
		||||
        , retry_(util::makeRetryExponentialBackoff(retryDelay, RETRY_MAX_DELAY, strand_))
 | 
			
		||||
        , onConnect_(std::move(onConnect))
 | 
			
		||||
        , onDisconnect_(std::move(onDisconnect))
 | 
			
		||||
        , onLedgerClosed_(std::move(onLedgerClosed))
 | 
			
		||||
    {
 | 
			
		||||
        wsConnectionBuilder_.addHeader({boost::beast::http::field::user_agent, "clio-client"})
 | 
			
		||||
            .addHeader({"X-User", "clio-client"})
 | 
			
		||||
            .setConnectionTimeout(connectionTimeout);
 | 
			
		||||
    }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Destroy the Subscription Source object
 | 
			
		||||
@@ -162,6 +149,14 @@ public:
 | 
			
		||||
    bool
 | 
			
		||||
    isConnected() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get whether the source is forwarding
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if the source is forwarding, false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    bool
 | 
			
		||||
    isForwarding() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set source forwarding
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -1,118 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/json/object.hpp>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
namespace etl::impl {
 | 
			
		||||
 | 
			
		||||
class SubscriptionSourceDependencies {
 | 
			
		||||
    struct Concept;
 | 
			
		||||
    std::unique_ptr<Concept> pImpl_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    template <typename NetworkValidatedLedgersType, typename SubscriptionManagerType>
 | 
			
		||||
    SubscriptionSourceDependencies(
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgersType> networkValidatedLedgers,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> subscriptions
 | 
			
		||||
    )
 | 
			
		||||
        : pImpl_{std::make_unique<Model<NetworkValidatedLedgersType, SubscriptionManagerType>>(
 | 
			
		||||
              std::move(networkValidatedLedgers),
 | 
			
		||||
              std::move(subscriptions)
 | 
			
		||||
          )}
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    forwardProposedTransaction(boost::json::object const& receivedTxJson)
 | 
			
		||||
    {
 | 
			
		||||
        pImpl_->forwardProposedTransaction(receivedTxJson);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    forwardValidation(boost::json::object const& validationJson) const
 | 
			
		||||
    {
 | 
			
		||||
        pImpl_->forwardValidation(validationJson);
 | 
			
		||||
    }
 | 
			
		||||
    void
 | 
			
		||||
    forwardManifest(boost::json::object const& manifestJson) const
 | 
			
		||||
    {
 | 
			
		||||
        pImpl_->forwardManifest(manifestJson);
 | 
			
		||||
    }
 | 
			
		||||
    void
 | 
			
		||||
    pushValidatedLedger(uint32_t const idx)
 | 
			
		||||
    {
 | 
			
		||||
        pImpl_->pushValidatedLedger(idx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    struct Concept {
 | 
			
		||||
        virtual ~Concept() = default;
 | 
			
		||||
 | 
			
		||||
        virtual void
 | 
			
		||||
        forwardProposedTransaction(boost::json::object const& receivedTxJson) = 0;
 | 
			
		||||
        virtual void
 | 
			
		||||
        forwardValidation(boost::json::object const& validationJson) const = 0;
 | 
			
		||||
        virtual void
 | 
			
		||||
        forwardManifest(boost::json::object const& manifestJson) const = 0;
 | 
			
		||||
        virtual void
 | 
			
		||||
        pushValidatedLedger(uint32_t idx) = 0;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    template <typename SomeNetworkValidatedLedgersType, typename SomeSubscriptionManagerType>
 | 
			
		||||
    class Model : public Concept {
 | 
			
		||||
        std::shared_ptr<SomeNetworkValidatedLedgersType> networkValidatedLedgers_;
 | 
			
		||||
        std::shared_ptr<SomeSubscriptionManagerType> subscriptions_;
 | 
			
		||||
 | 
			
		||||
    public:
 | 
			
		||||
        Model(
 | 
			
		||||
            std::shared_ptr<SomeNetworkValidatedLedgersType> networkValidatedLedgers,
 | 
			
		||||
            std::shared_ptr<SomeSubscriptionManagerType> subscriptions
 | 
			
		||||
        )
 | 
			
		||||
            : networkValidatedLedgers_{std::move(networkValidatedLedgers)}, subscriptions_{std::move(subscriptions)}
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        void
 | 
			
		||||
        forwardProposedTransaction(boost::json::object const& receivedTxJson) override
 | 
			
		||||
        {
 | 
			
		||||
            subscriptions_->forwardProposedTransaction(receivedTxJson);
 | 
			
		||||
        }
 | 
			
		||||
        void
 | 
			
		||||
        forwardValidation(boost::json::object const& validationJson) const override
 | 
			
		||||
        {
 | 
			
		||||
            subscriptions_->forwardValidation(validationJson);
 | 
			
		||||
        }
 | 
			
		||||
        void
 | 
			
		||||
        forwardManifest(boost::json::object const& manifestJson) const override
 | 
			
		||||
        {
 | 
			
		||||
            subscriptions_->forwardManifest(manifestJson);
 | 
			
		||||
        }
 | 
			
		||||
        void
 | 
			
		||||
        pushValidatedLedger(uint32_t idx) override
 | 
			
		||||
        {
 | 
			
		||||
            networkValidatedLedgers_->push(idx);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace etl::impl
 | 
			
		||||
@@ -186,4 +186,20 @@ SubscriptionManager::pubTransaction(data::TransactionAndMetadata const& txMeta,
 | 
			
		||||
    transactionFeed_.pub(txMeta, lgrInfo, backend_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
boost::json::object
 | 
			
		||||
SubscriptionManager::report() const
 | 
			
		||||
{
 | 
			
		||||
    return {
 | 
			
		||||
        {"ledger", ledgerFeed_.count()},
 | 
			
		||||
        {"transactions", transactionFeed_.transactionSubCount()},
 | 
			
		||||
        {"transactions_proposed", proposedTransactionFeed_.transactionSubcount()},
 | 
			
		||||
        {"manifests", manifestFeed_.count()},
 | 
			
		||||
        {"validations", validationsFeed_.count()},
 | 
			
		||||
        {"account", transactionFeed_.accountSubCount()},
 | 
			
		||||
        {"accounts_proposed", proposedTransactionFeed_.accountSubCount()},
 | 
			
		||||
        {"books", transactionFeed_.bookSubCount()},
 | 
			
		||||
        {"book_changes", bookChangesFeed_.count()},
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace feed
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "data/Types.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "feed/Types.hpp"
 | 
			
		||||
#include "feed/impl/BookChangesFeed.hpp"
 | 
			
		||||
#include "feed/impl/ForwardFeed.hpp"
 | 
			
		||||
@@ -55,7 +56,7 @@ namespace feed {
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A subscription manager is responsible for managing the subscriptions and publishing the feeds
 | 
			
		||||
 */
 | 
			
		||||
class SubscriptionManager {
 | 
			
		||||
class SubscriptionManager : public SubscriptionManagerInterface {
 | 
			
		||||
    std::reference_wrapper<boost::asio::io_context> ioContext_;
 | 
			
		||||
    std::shared_ptr<data::BackendInterface const> backend_;
 | 
			
		||||
 | 
			
		||||
@@ -93,14 +94,14 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subBookChanges(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subBookChanges(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the book changes feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubBookChanges(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubBookChanges(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Publish the book changes feed.
 | 
			
		||||
@@ -109,21 +110,21 @@ public:
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    pubBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions)
 | 
			
		||||
        const;
 | 
			
		||||
        const final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the proposed transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subProposedTransactions(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subProposedTransactions(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the proposed transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubProposedTransactions(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubProposedTransactions(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the proposed transactions feed, only receive the feed when particular account is affected.
 | 
			
		||||
@@ -131,7 +132,7 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the proposed transactions feed for particular account.
 | 
			
		||||
@@ -139,14 +140,14 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the proposed transactions feed.
 | 
			
		||||
     * @param receivedTxJson The proposed transaction json.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    forwardProposedTransaction(boost::json::object const& receivedTxJson);
 | 
			
		||||
    forwardProposedTransaction(boost::json::object const& receivedTxJson) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the ledger feed.
 | 
			
		||||
@@ -155,14 +156,14 @@ public:
 | 
			
		||||
     * @return The ledger feed
 | 
			
		||||
     */
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    subLedger(boost::asio::yield_context yield, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subLedger(boost::asio::yield_context yield, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the ledger feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubLedger(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubLedger(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Publish the ledger feed.
 | 
			
		||||
@@ -177,63 +178,63 @@ public:
 | 
			
		||||
        ripple::Fees const& fees,
 | 
			
		||||
        std::string const& ledgerRange,
 | 
			
		||||
        std::uint32_t txnCount
 | 
			
		||||
    ) const;
 | 
			
		||||
    ) const final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the manifest feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subManifest(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subManifest(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the manifest feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubManifest(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubManifest(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the manifest feed.
 | 
			
		||||
     * @param manifestJson The manifest json to forward.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    forwardManifest(boost::json::object const& manifestJson) const;
 | 
			
		||||
    forwardManifest(boost::json::object const& manifestJson) const final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the validation feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subValidation(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subValidation(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the validation feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubValidation(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubValidation(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the validation feed.
 | 
			
		||||
     * @param validationJson The validation feed json to forward.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    forwardValidation(boost::json::object const& validationJson) const;
 | 
			
		||||
    forwardValidation(boost::json::object const& validationJson) const final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subTransactions(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subTransactions(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubTransactions(SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubTransactions(SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed, only receive the feed when particular account is affected.
 | 
			
		||||
@@ -241,7 +242,7 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed for particular account.
 | 
			
		||||
@@ -249,7 +250,7 @@ public:
 | 
			
		||||
     * @param subscriber The subscriber to unsubscribe
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed, only receive feed when particular order book is affected.
 | 
			
		||||
@@ -257,7 +258,7 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    subBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    subBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed for particular order book.
 | 
			
		||||
@@ -265,7 +266,7 @@ public:
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    unsubBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber);
 | 
			
		||||
    unsubBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the transactions feed.
 | 
			
		||||
@@ -273,7 +274,7 @@ public:
 | 
			
		||||
     * @param lgrInfo The ledger header.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    pubTransaction(data::TransactionAndMetadata const& txMeta, ripple::LedgerHeader const& lgrInfo);
 | 
			
		||||
    pubTransaction(data::TransactionAndMetadata const& txMeta, ripple::LedgerHeader const& lgrInfo) final;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get the number of subscribers.
 | 
			
		||||
@@ -281,20 +282,7 @@ public:
 | 
			
		||||
     * @return The report of the number of subscribers
 | 
			
		||||
     */
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    report() const
 | 
			
		||||
    {
 | 
			
		||||
        return {
 | 
			
		||||
            {"ledger", ledgerFeed_.count()},
 | 
			
		||||
            {"transactions", transactionFeed_.transactionSubCount()},
 | 
			
		||||
            {"transactions_proposed", proposedTransactionFeed_.transactionSubcount()},
 | 
			
		||||
            {"manifests", manifestFeed_.count()},
 | 
			
		||||
            {"validations", validationsFeed_.count()},
 | 
			
		||||
            {"account", transactionFeed_.accountSubCount()},
 | 
			
		||||
            {"accounts_proposed", proposedTransactionFeed_.accountSubCount()},
 | 
			
		||||
            {"books", transactionFeed_.bookSubCount()},
 | 
			
		||||
            {"book_changes", bookChangesFeed_.count()},
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    report() const final;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										244
									
								
								src/feed/SubscriptionManagerInterface.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								src/feed/SubscriptionManagerInterface.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,244 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/Types.hpp"
 | 
			
		||||
#include "feed/Types.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <ripple/protocol/AccountID.h>
 | 
			
		||||
#include <ripple/protocol/Book.h>
 | 
			
		||||
#include <ripple/protocol/Fees.h>
 | 
			
		||||
#include <ripple/protocol/LedgerHeader.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace feed {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Interface of subscription manager.
 | 
			
		||||
 * A subscription manager is responsible for managing the subscriptions and publishing the feeds.
 | 
			
		||||
 */
 | 
			
		||||
class SubscriptionManagerInterface {
 | 
			
		||||
public:
 | 
			
		||||
    virtual ~SubscriptionManagerInterface() = default;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the book changes feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subBookChanges(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the book changes feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubBookChanges(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Publish the book changes feed.
 | 
			
		||||
     * @param lgrInfo The current ledger header.
 | 
			
		||||
     * @param transactions The transactions in the current ledger.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    pubBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions)
 | 
			
		||||
        const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the proposed transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subProposedTransactions(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the proposed transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubProposedTransactions(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the proposed transactions feed, only receive the feed when particular account is affected.
 | 
			
		||||
     * @param account The account to watch.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the proposed transactions feed for particular account.
 | 
			
		||||
     * @param account The account to stop watching.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the proposed transactions feed.
 | 
			
		||||
     * @param receivedTxJson The proposed transaction json.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    forwardProposedTransaction(boost::json::object const& receivedTxJson) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the ledger feed.
 | 
			
		||||
     * @param yield The coroutine context
 | 
			
		||||
     * @param subscriber The subscriber to the ledger feed
 | 
			
		||||
     * @return The ledger feed
 | 
			
		||||
     */
 | 
			
		||||
    virtual boost::json::object
 | 
			
		||||
    subLedger(boost::asio::yield_context yield, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the ledger feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubLedger(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Publish the ledger feed.
 | 
			
		||||
     * @param lgrInfo The ledger header.
 | 
			
		||||
     * @param fees The fees.
 | 
			
		||||
     * @param ledgerRange The ledger range.
 | 
			
		||||
     * @param txnCount The transaction count.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    pubLedger(
 | 
			
		||||
        ripple::LedgerHeader const& lgrInfo,
 | 
			
		||||
        ripple::Fees const& fees,
 | 
			
		||||
        std::string const& ledgerRange,
 | 
			
		||||
        std::uint32_t txnCount
 | 
			
		||||
    ) const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the manifest feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subManifest(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the manifest feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubManifest(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the manifest feed.
 | 
			
		||||
     * @param manifestJson The manifest json to forward.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    forwardManifest(boost::json::object const& manifestJson) const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the validation feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subValidation(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the validation feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubValidation(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the validation feed.
 | 
			
		||||
     * @param validationJson The validation feed json to forward.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    forwardValidation(boost::json::object const& validationJson) const = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subTransactions(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubTransactions(SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed, only receive the feed when particular account is affected.
 | 
			
		||||
     * @param account The account to watch.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed for particular account.
 | 
			
		||||
     * @param account The account to stop watching
 | 
			
		||||
     * @param subscriber The subscriber to unsubscribe
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Subscribe to the transactions feed, only receive feed when particular order book is affected.
 | 
			
		||||
     * @param book The book to watch.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    subBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Unsubscribe to the transactions feed for particular order book.
 | 
			
		||||
     * @param book The book to watch.
 | 
			
		||||
     * @param subscriber
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    unsubBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Forward the transactions feed.
 | 
			
		||||
     * @param txMeta The transaction and metadata.
 | 
			
		||||
     * @param lgrInfo The ledger header.
 | 
			
		||||
     */
 | 
			
		||||
    virtual void
 | 
			
		||||
    pubTransaction(data::TransactionAndMetadata const& txMeta, ripple::LedgerHeader const& lgrInfo) = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get the number of subscribers.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The report of the number of subscribers
 | 
			
		||||
     */
 | 
			
		||||
    virtual boost::json::object
 | 
			
		||||
    report() const = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace feed
 | 
			
		||||
@@ -18,8 +18,8 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "data/BackendFactory.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "etl/ETLService.hpp"
 | 
			
		||||
#include "etl/NetworkValidatedLedgers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManager.hpp"
 | 
			
		||||
#include "main/Build.hpp"
 | 
			
		||||
#include "rpc/Counters.hpp"
 | 
			
		||||
@@ -34,7 +34,6 @@
 | 
			
		||||
#include "web/IntervalSweepHandler.hpp"
 | 
			
		||||
#include "web/RPCServerHandler.hpp"
 | 
			
		||||
#include "web/Server.hpp"
 | 
			
		||||
#include "web/WhitelistHandler.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,9 @@ target_sources(
 | 
			
		||||
          handlers/NFTSellOffers.cpp
 | 
			
		||||
          handlers/NoRippleCheck.cpp
 | 
			
		||||
          handlers/Random.cpp
 | 
			
		||||
          handlers/Subscribe.cpp
 | 
			
		||||
          handlers/TransactionEntry.cpp
 | 
			
		||||
          handlers/Unsubscribe.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(clio_rpc PRIVATE clio_util)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "data/DBHelpers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "main/Build.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
@@ -51,9 +52,6 @@ namespace etl {
 | 
			
		||||
class ETLService;
 | 
			
		||||
class LoadBalancer;
 | 
			
		||||
}  // namespace etl
 | 
			
		||||
namespace feed {
 | 
			
		||||
class SubscriptionManager;
 | 
			
		||||
}  // namespace feed
 | 
			
		||||
namespace rpc {
 | 
			
		||||
class Counters;
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
@@ -63,17 +61,16 @@ namespace rpc {
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Contains common functionality for handling the `server_info` command
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam SubscriptionManagerType The type of the subscription manager
 | 
			
		||||
 * @tparam LoadBalancerType The type of the load balancer
 | 
			
		||||
 * @tparam ETLServiceType The type of the ETL service
 | 
			
		||||
 * @tparam CountersType The type of the counters
 | 
			
		||||
 */
 | 
			
		||||
template <typename SubscriptionManagerType, typename LoadBalancerType, typename ETLServiceType, typename CountersType>
 | 
			
		||||
template <typename LoadBalancerType, typename ETLServiceType, typename CountersType>
 | 
			
		||||
class BaseServerInfoHandler {
 | 
			
		||||
    static constexpr auto BACKEND_COUNTERS_KEY = "backend_counters";
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend_;
 | 
			
		||||
    std::shared_ptr<SubscriptionManagerType> subscriptions_;
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
 | 
			
		||||
    std::shared_ptr<LoadBalancerType> balancer_;
 | 
			
		||||
    std::shared_ptr<ETLServiceType const> etl_;
 | 
			
		||||
    std::reference_wrapper<CountersType const> counters_;
 | 
			
		||||
@@ -159,7 +156,7 @@ public:
 | 
			
		||||
     */
 | 
			
		||||
    BaseServerInfoHandler(
 | 
			
		||||
        std::shared_ptr<BackendInterface> const& backend,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> const& subscriptions,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions,
 | 
			
		||||
        std::shared_ptr<LoadBalancerType> const& balancer,
 | 
			
		||||
        std::shared_ptr<ETLServiceType const> const& etl,
 | 
			
		||||
        CountersType const& counters
 | 
			
		||||
@@ -352,7 +349,6 @@ private:
 | 
			
		||||
 *
 | 
			
		||||
 * For more details see: https://xrpl.org/server_info-clio.html
 | 
			
		||||
 */
 | 
			
		||||
using ServerInfoHandler =
 | 
			
		||||
    BaseServerInfoHandler<feed::SubscriptionManager, etl::LoadBalancer, etl::ETLService, Counters>;
 | 
			
		||||
using ServerInfoHandler = BaseServerInfoHandler<etl::LoadBalancer, etl::ETLService, Counters>;
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										303
									
								
								src/rpc/handlers/Subscribe.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/rpc/handlers/Subscribe.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,303 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/handlers/Subscribe.hpp"
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "data/Types.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "rpc/common/MetaProcessors.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <ripple/beast/utility/Zero.h>
 | 
			
		||||
#include <ripple/protocol/Book.h>
 | 
			
		||||
#include <ripple/protocol/ErrorCodes.h>
 | 
			
		||||
#include <ripple/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <variant>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
SubscribeHandler::SubscribeHandler(
 | 
			
		||||
    std::shared_ptr<BackendInterface> const& sharedPtrBackend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
 | 
			
		||||
)
 | 
			
		||||
    : sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RpcSpecConstRef
 | 
			
		||||
SubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
{
 | 
			
		||||
    static auto const booksValidator =
 | 
			
		||||
        validation::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"}};
 | 
			
		||||
 | 
			
		||||
            for (auto const& book : value.as_array()) {
 | 
			
		||||
                if (!book.is_object())
 | 
			
		||||
                    return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
 | 
			
		||||
 | 
			
		||||
                if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
 | 
			
		||||
                    return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
 | 
			
		||||
 | 
			
		||||
                if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool())
 | 
			
		||||
                    return Error{Status{RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}};
 | 
			
		||||
 | 
			
		||||
                if (book.as_object().contains("taker")) {
 | 
			
		||||
                    if (auto err = meta::WithCustomError(
 | 
			
		||||
                                       validation::AccountValidator,
 | 
			
		||||
                                       Status{RippledError::rpcBAD_ISSUER, "Issuer account malformed."}
 | 
			
		||||
                        )
 | 
			
		||||
                                       .verify(book.as_object(), "taker");
 | 
			
		||||
                        !err)
 | 
			
		||||
                        return err;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                auto const parsedBook = parseBook(book.as_object());
 | 
			
		||||
                if (auto const status = std::get_if<Status>(&parsedBook))
 | 
			
		||||
                    return Error(*status);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return MaybeError{};
 | 
			
		||||
        }};
 | 
			
		||||
 | 
			
		||||
    static auto const rpcSpec = RpcSpec{
 | 
			
		||||
        {JS(streams), validation::SubscribeStreamValidator},
 | 
			
		||||
        {JS(accounts), validation::SubscribeAccountsValidator},
 | 
			
		||||
        {JS(accounts_proposed), validation::SubscribeAccountsValidator},
 | 
			
		||||
        {JS(books), booksValidator},
 | 
			
		||||
        {"user", check::Deprecated{}},
 | 
			
		||||
        {JS(password), check::Deprecated{}},
 | 
			
		||||
        {JS(rt_accounts), check::Deprecated{}}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return rpcSpec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SubscribeHandler::Result
 | 
			
		||||
SubscribeHandler::process(Input input, Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    auto output = Output{};
 | 
			
		||||
 | 
			
		||||
    // Mimic rippled. No matter what the request is, the api version changes for the whole session
 | 
			
		||||
    ctx.session->apiSubVersion = ctx.apiVersion;
 | 
			
		||||
 | 
			
		||||
    if (input.streams) {
 | 
			
		||||
        auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
 | 
			
		||||
        if (!ledger.empty())
 | 
			
		||||
            output.ledger = ledger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (input.accounts)
 | 
			
		||||
        subscribeToAccounts(*(input.accounts), ctx.session);
 | 
			
		||||
 | 
			
		||||
    if (input.accountsProposed)
 | 
			
		||||
        subscribeToAccountsProposed(*(input.accountsProposed), ctx.session);
 | 
			
		||||
 | 
			
		||||
    if (input.books)
 | 
			
		||||
        subscribeToBooks(*(input.books), ctx.session, ctx.yield, output);
 | 
			
		||||
 | 
			
		||||
    return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
boost::json::object
 | 
			
		||||
SubscribeHandler::subscribeToStreams(
 | 
			
		||||
    boost::asio::yield_context yield,
 | 
			
		||||
    std::vector<std::string> const& streams,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    auto response = boost::json::object{};
 | 
			
		||||
 | 
			
		||||
    for (auto const& stream : streams) {
 | 
			
		||||
        if (stream == "ledger") {
 | 
			
		||||
            response = subscriptions_->subLedger(yield, session);
 | 
			
		||||
        } else if (stream == "transactions") {
 | 
			
		||||
            subscriptions_->subTransactions(session);
 | 
			
		||||
        } else if (stream == "transactions_proposed") {
 | 
			
		||||
            subscriptions_->subProposedTransactions(session);
 | 
			
		||||
        } else if (stream == "validations") {
 | 
			
		||||
            subscriptions_->subValidation(session);
 | 
			
		||||
        } else if (stream == "manifests") {
 | 
			
		||||
            subscriptions_->subManifest(session);
 | 
			
		||||
        } else if (stream == "book_changes") {
 | 
			
		||||
            subscriptions_->subBookChanges(session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscribeHandler::subscribeToAccountsProposed(
 | 
			
		||||
    std::vector<std::string> const& accounts,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& account : accounts) {
 | 
			
		||||
        auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
        subscriptions_->subProposedAccount(*accountID, session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscribeHandler::subscribeToAccounts(
 | 
			
		||||
    std::vector<std::string> const& accounts,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& account : accounts) {
 | 
			
		||||
        auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
        subscriptions_->subAccount(*accountID, session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscribeHandler::subscribeToBooks(
 | 
			
		||||
    std::vector<OrderBook> const& books,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session,
 | 
			
		||||
    boost::asio::yield_context yield,
 | 
			
		||||
    Output& output
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    static auto constexpr fetchLimit = 200;
 | 
			
		||||
 | 
			
		||||
    std::optional<data::LedgerRange> rng;
 | 
			
		||||
 | 
			
		||||
    for (auto const& internalBook : books) {
 | 
			
		||||
        if (internalBook.snapshot) {
 | 
			
		||||
            if (!rng)
 | 
			
		||||
                rng = sharedPtrBackend_->fetchLedgerRange();
 | 
			
		||||
 | 
			
		||||
            auto const getOrderBook = [&](auto const& book, auto& snapshots) {
 | 
			
		||||
                auto const bookBase = getBookBase(book);
 | 
			
		||||
                auto const [offers, _] =
 | 
			
		||||
                    sharedPtrBackend_->fetchBookOffers(bookBase, rng->maxSequence, fetchLimit, yield);
 | 
			
		||||
 | 
			
		||||
                // the taker is not really uesed, same issue with
 | 
			
		||||
                // https://github.com/XRPLF/xrpl-dev-portal/issues/1818
 | 
			
		||||
                auto const takerID = internalBook.taker ? accountFromStringStrict(*(internalBook.taker)) : beast::zero;
 | 
			
		||||
 | 
			
		||||
                auto const orderBook =
 | 
			
		||||
                    postProcessOrderBook(offers, book, *takerID, *sharedPtrBackend_, rng->maxSequence, yield);
 | 
			
		||||
                std::copy(orderBook.begin(), orderBook.end(), std::back_inserter(snapshots));
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (internalBook.both) {
 | 
			
		||||
                if (!output.bids)
 | 
			
		||||
                    output.bids = boost::json::array();
 | 
			
		||||
                if (!output.asks)
 | 
			
		||||
                    output.asks = boost::json::array();
 | 
			
		||||
                getOrderBook(internalBook.book, *(output.bids));
 | 
			
		||||
                getOrderBook(ripple::reversed(internalBook.book), *(output.asks));
 | 
			
		||||
            } else {
 | 
			
		||||
                if (!output.offers)
 | 
			
		||||
                    output.offers = boost::json::array();
 | 
			
		||||
                getOrderBook(internalBook.book, *(output.offers));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        subscriptions_->subBook(internalBook.book, session);
 | 
			
		||||
 | 
			
		||||
        if (internalBook.both)
 | 
			
		||||
            subscriptions_->subBook(ripple::reversed(internalBook.book), session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, SubscribeHandler::Output const& output)
 | 
			
		||||
{
 | 
			
		||||
    jv = output.ledger ? *(output.ledger) : boost::json::object();
 | 
			
		||||
 | 
			
		||||
    if (output.offers)
 | 
			
		||||
        jv.as_object().emplace(JS(offers), *(output.offers));
 | 
			
		||||
    if (output.asks)
 | 
			
		||||
        jv.as_object().emplace(JS(asks), *(output.asks));
 | 
			
		||||
    if (output.bids)
 | 
			
		||||
        jv.as_object().emplace(JS(bids), *(output.bids));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SubscribeHandler::Input
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<SubscribeHandler::Input>, boost::json::value const& jv)
 | 
			
		||||
{
 | 
			
		||||
    auto input = SubscribeHandler::Input{};
 | 
			
		||||
    auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
    if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
 | 
			
		||||
        input.streams = std::vector<std::string>();
 | 
			
		||||
        for (auto const& stream : streams->value().as_array())
 | 
			
		||||
            input.streams->push_back(boost::json::value_to<std::string>(stream));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
 | 
			
		||||
        input.accounts = std::vector<std::string>();
 | 
			
		||||
        for (auto const& account : accounts->value().as_array())
 | 
			
		||||
            input.accounts->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end()) {
 | 
			
		||||
        input.accountsProposed = std::vector<std::string>();
 | 
			
		||||
        for (auto const& account : accountsProposed->value().as_array())
 | 
			
		||||
            input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
 | 
			
		||||
        input.books = std::vector<SubscribeHandler::OrderBook>();
 | 
			
		||||
        for (auto const& book : books->value().as_array()) {
 | 
			
		||||
            auto internalBook = SubscribeHandler::OrderBook{};
 | 
			
		||||
            auto const& bookObject = book.as_object();
 | 
			
		||||
 | 
			
		||||
            if (auto const& taker = bookObject.find(JS(taker)); taker != bookObject.end())
 | 
			
		||||
                internalBook.taker = boost::json::value_to<std::string>(taker->value());
 | 
			
		||||
 | 
			
		||||
            if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
 | 
			
		||||
                internalBook.both = both->value().as_bool();
 | 
			
		||||
 | 
			
		||||
            if (auto const& snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end())
 | 
			
		||||
                internalBook.snapshot = snapshot->value().as_bool();
 | 
			
		||||
 | 
			
		||||
            auto const parsedBookMaybe = parseBook(book.as_object());
 | 
			
		||||
            internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
            input.books->push_back(internalBook);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return input;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
@@ -20,14 +20,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "data/Types.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "rpc/common/MetaProcessors.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
@@ -44,25 +39,20 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <variant>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace feed {
 | 
			
		||||
class SubscriptionManager;
 | 
			
		||||
}  // namespace feed
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Contains functionality for handling the `subscribe` command
 | 
			
		||||
 * @brief Contains functionality for handling the `subscribe` command.
 | 
			
		||||
 * The subscribe method requests periodic notifications from the server when certain events happen.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam SubscriptionManagerType The type of the subscription manager to use
 | 
			
		||||
 * For more details see: https://xrpl.org/subscribe.html
 | 
			
		||||
 */
 | 
			
		||||
template <typename SubscriptionManagerType>
 | 
			
		||||
class BaseSubscribeHandler {
 | 
			
		||||
 | 
			
		||||
class SubscribeHandler {
 | 
			
		||||
    std::shared_ptr<BackendInterface> sharedPtrBackend_;
 | 
			
		||||
    std::shared_ptr<SubscriptionManagerType> subscriptions_;
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
@@ -109,13 +99,10 @@ public:
 | 
			
		||||
     * @param sharedPtrBackend The backend to use
 | 
			
		||||
     * @param subscriptions The subscription manager to use
 | 
			
		||||
     */
 | 
			
		||||
    BaseSubscribeHandler(
 | 
			
		||||
    SubscribeHandler(
 | 
			
		||||
        std::shared_ptr<BackendInterface> const& sharedPtrBackend,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> const& subscriptions
 | 
			
		||||
    )
 | 
			
		||||
        : sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Returns the API specification for the command
 | 
			
		||||
@@ -124,53 +111,7 @@ public:
 | 
			
		||||
     * @return The spec for the given apiVersion
 | 
			
		||||
     */
 | 
			
		||||
    static RpcSpecConstRef
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
    {
 | 
			
		||||
        static auto const booksValidator =
 | 
			
		||||
            validation::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"}};
 | 
			
		||||
 | 
			
		||||
                for (auto const& book : value.as_array()) {
 | 
			
		||||
                    if (!book.is_object())
 | 
			
		||||
                        return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
 | 
			
		||||
 | 
			
		||||
                    if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
 | 
			
		||||
                        return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
 | 
			
		||||
 | 
			
		||||
                    if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool())
 | 
			
		||||
                        return Error{Status{RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}};
 | 
			
		||||
 | 
			
		||||
                    if (book.as_object().contains("taker")) {
 | 
			
		||||
                        if (auto err = meta::WithCustomError(
 | 
			
		||||
                                           validation::AccountValidator,
 | 
			
		||||
                                           Status{RippledError::rpcBAD_ISSUER, "Issuer account malformed."}
 | 
			
		||||
                            )
 | 
			
		||||
                                           .verify(book.as_object(), "taker");
 | 
			
		||||
                            !err)
 | 
			
		||||
                            return err;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    auto const parsedBook = parseBook(book.as_object());
 | 
			
		||||
                    if (auto const status = std::get_if<Status>(&parsedBook))
 | 
			
		||||
                        return Error(*status);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return MaybeError{};
 | 
			
		||||
            }};
 | 
			
		||||
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(streams), validation::SubscribeStreamValidator},
 | 
			
		||||
            {JS(accounts), validation::SubscribeAccountsValidator},
 | 
			
		||||
            {JS(accounts_proposed), validation::SubscribeAccountsValidator},
 | 
			
		||||
            {JS(books), booksValidator},
 | 
			
		||||
            {"user", check::Deprecated{}},
 | 
			
		||||
            {JS(password), check::Deprecated{}},
 | 
			
		||||
            {JS(rt_accounts), check::Deprecated{}}
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return rpcSpec;
 | 
			
		||||
    }
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Process the Subscribe command
 | 
			
		||||
@@ -180,30 +121,7 @@ public:
 | 
			
		||||
     * @return The result of the operation
 | 
			
		||||
     */
 | 
			
		||||
    Result
 | 
			
		||||
    process(Input input, Context const& ctx) const
 | 
			
		||||
    {
 | 
			
		||||
        auto output = Output{};
 | 
			
		||||
 | 
			
		||||
        // Mimic rippled. No matter what the request is, the api version changes for the whole session
 | 
			
		||||
        ctx.session->apiSubVersion = ctx.apiVersion;
 | 
			
		||||
 | 
			
		||||
        if (input.streams) {
 | 
			
		||||
            auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
 | 
			
		||||
            if (!ledger.empty())
 | 
			
		||||
                output.ledger = ledger;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (input.accounts)
 | 
			
		||||
            subscribeToAccounts(*(input.accounts), ctx.session);
 | 
			
		||||
 | 
			
		||||
        if (input.accountsProposed)
 | 
			
		||||
            subscribeToAccountsProposed(*(input.accountsProposed), ctx.session);
 | 
			
		||||
 | 
			
		||||
        if (input.books)
 | 
			
		||||
            subscribeToBooks(*(input.books), ctx.session, ctx.yield, output);
 | 
			
		||||
 | 
			
		||||
        return output;
 | 
			
		||||
    }
 | 
			
		||||
    process(Input input, Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    boost::json::object
 | 
			
		||||
@@ -211,50 +129,17 @@ private:
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        std::vector<std::string> const& streams,
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
    ) const
 | 
			
		||||
    {
 | 
			
		||||
        auto response = boost::json::object{};
 | 
			
		||||
 | 
			
		||||
        for (auto const& stream : streams) {
 | 
			
		||||
            if (stream == "ledger") {
 | 
			
		||||
                response = subscriptions_->subLedger(yield, session);
 | 
			
		||||
            } else if (stream == "transactions") {
 | 
			
		||||
                subscriptions_->subTransactions(session);
 | 
			
		||||
            } else if (stream == "transactions_proposed") {
 | 
			
		||||
                subscriptions_->subProposedTransactions(session);
 | 
			
		||||
            } else if (stream == "validations") {
 | 
			
		||||
                subscriptions_->subValidation(session);
 | 
			
		||||
            } else if (stream == "manifests") {
 | 
			
		||||
                subscriptions_->subManifest(session);
 | 
			
		||||
            } else if (stream == "book_changes") {
 | 
			
		||||
                subscriptions_->subBookChanges(session);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
    ) const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    subscribeToAccounts(std::vector<std::string> const& accounts, std::shared_ptr<web::ConnectionBase> const& session)
 | 
			
		||||
        const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& account : accounts) {
 | 
			
		||||
            auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
            subscriptions_->subAccount(*accountID, session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    subscribeToAccountsProposed(
 | 
			
		||||
        std::vector<std::string> const& accounts,
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
    ) const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& account : accounts) {
 | 
			
		||||
            auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
            subscriptions_->subProposedAccount(*accountID, session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    ) const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    subscribeToBooks(
 | 
			
		||||
@@ -262,121 +147,25 @@ private:
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& session,
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        Output& output
 | 
			
		||||
    ) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto constexpr fetchLimit = 200;
 | 
			
		||||
 | 
			
		||||
        std::optional<data::LedgerRange> rng;
 | 
			
		||||
 | 
			
		||||
        for (auto const& internalBook : books) {
 | 
			
		||||
            if (internalBook.snapshot) {
 | 
			
		||||
                if (!rng)
 | 
			
		||||
                    rng = sharedPtrBackend_->fetchLedgerRange();
 | 
			
		||||
 | 
			
		||||
                auto const getOrderBook = [&](auto const& book, auto& snapshots) {
 | 
			
		||||
                    auto const bookBase = getBookBase(book);
 | 
			
		||||
                    auto const [offers, _] =
 | 
			
		||||
                        sharedPtrBackend_->fetchBookOffers(bookBase, rng->maxSequence, fetchLimit, yield);
 | 
			
		||||
 | 
			
		||||
                    // the taker is not really uesed, same issue with
 | 
			
		||||
                    // https://github.com/XRPLF/xrpl-dev-portal/issues/1818
 | 
			
		||||
                    auto const takerID =
 | 
			
		||||
                        internalBook.taker ? accountFromStringStrict(*(internalBook.taker)) : beast::zero;
 | 
			
		||||
 | 
			
		||||
                    auto const orderBook =
 | 
			
		||||
                        postProcessOrderBook(offers, book, *takerID, *sharedPtrBackend_, rng->maxSequence, yield);
 | 
			
		||||
                    std::copy(orderBook.begin(), orderBook.end(), std::back_inserter(snapshots));
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if (internalBook.both) {
 | 
			
		||||
                    if (!output.bids)
 | 
			
		||||
                        output.bids = boost::json::array();
 | 
			
		||||
                    if (!output.asks)
 | 
			
		||||
                        output.asks = boost::json::array();
 | 
			
		||||
                    getOrderBook(internalBook.book, *(output.bids));
 | 
			
		||||
                    getOrderBook(ripple::reversed(internalBook.book), *(output.asks));
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (!output.offers)
 | 
			
		||||
                        output.offers = boost::json::array();
 | 
			
		||||
                    getOrderBook(internalBook.book, *(output.offers));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            subscriptions_->subBook(internalBook.book, session);
 | 
			
		||||
 | 
			
		||||
            if (internalBook.both)
 | 
			
		||||
                subscriptions_->subBook(ripple::reversed(internalBook.book), session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    ) const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Convert output to json value
 | 
			
		||||
     *
 | 
			
		||||
     * @param jv The json value to convert to
 | 
			
		||||
     * @param output The output to convert from
 | 
			
		||||
     */
 | 
			
		||||
    friend void
 | 
			
		||||
    tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output)
 | 
			
		||||
    {
 | 
			
		||||
        jv = output.ledger ? *(output.ledger) : boost::json::object();
 | 
			
		||||
 | 
			
		||||
        if (output.offers)
 | 
			
		||||
            jv.as_object().emplace(JS(offers), *(output.offers));
 | 
			
		||||
        if (output.asks)
 | 
			
		||||
            jv.as_object().emplace(JS(asks), *(output.asks));
 | 
			
		||||
        if (output.bids)
 | 
			
		||||
            jv.as_object().emplace(JS(bids), *(output.bids));
 | 
			
		||||
    }
 | 
			
		||||
    tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Convert json value to input
 | 
			
		||||
     *
 | 
			
		||||
     * @param jv The json value to convert from
 | 
			
		||||
     * @return The input to convert to
 | 
			
		||||
     */
 | 
			
		||||
    friend Input
 | 
			
		||||
    tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv)
 | 
			
		||||
    {
 | 
			
		||||
        auto input = Input{};
 | 
			
		||||
        auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
        if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
 | 
			
		||||
            input.streams = std::vector<std::string>();
 | 
			
		||||
            for (auto const& stream : streams->value().as_array())
 | 
			
		||||
                input.streams->push_back(boost::json::value_to<std::string>(stream));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
 | 
			
		||||
            input.accounts = std::vector<std::string>();
 | 
			
		||||
            for (auto const& account : accounts->value().as_array())
 | 
			
		||||
                input.accounts->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed));
 | 
			
		||||
            accountsProposed != jsonObject.end()) {
 | 
			
		||||
            input.accountsProposed = std::vector<std::string>();
 | 
			
		||||
            for (auto const& account : accountsProposed->value().as_array())
 | 
			
		||||
                input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
 | 
			
		||||
            input.books = std::vector<OrderBook>();
 | 
			
		||||
            for (auto const& book : books->value().as_array()) {
 | 
			
		||||
                auto internalBook = OrderBook{};
 | 
			
		||||
                auto const& bookObject = book.as_object();
 | 
			
		||||
 | 
			
		||||
                if (auto const& taker = bookObject.find(JS(taker)); taker != bookObject.end())
 | 
			
		||||
                    internalBook.taker = boost::json::value_to<std::string>(taker->value());
 | 
			
		||||
 | 
			
		||||
                if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
 | 
			
		||||
                    internalBook.both = both->value().as_bool();
 | 
			
		||||
 | 
			
		||||
                if (auto const& snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end())
 | 
			
		||||
                    internalBook.snapshot = snapshot->value().as_bool();
 | 
			
		||||
 | 
			
		||||
                auto const parsedBookMaybe = parseBook(book.as_object());
 | 
			
		||||
                internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
                input.books->push_back(internalBook);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return input;
 | 
			
		||||
    }
 | 
			
		||||
    tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The subscribe method requests periodic notifications from the server when certain events happen.
 | 
			
		||||
 *
 | 
			
		||||
 * For more details see: https://xrpl.org/subscribe.html
 | 
			
		||||
 */
 | 
			
		||||
using SubscribeHandler = BaseSubscribeHandler<feed::SubscriptionManager>;
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										208
									
								
								src/rpc/handlers/Unsubscribe.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								src/rpc/handlers/Unsubscribe.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/handlers/Unsubscribe.hpp"
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <ripple/protocol/Book.h>
 | 
			
		||||
#include <ripple/protocol/ErrorCodes.h>
 | 
			
		||||
#include <ripple/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <variant>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
UnsubscribeHandler::UnsubscribeHandler(
 | 
			
		||||
    std::shared_ptr<BackendInterface> const& sharedPtrBackend,
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
 | 
			
		||||
)
 | 
			
		||||
    : sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RpcSpecConstRef
 | 
			
		||||
UnsubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
{
 | 
			
		||||
    static auto const booksValidator =
 | 
			
		||||
        validation::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"}};
 | 
			
		||||
 | 
			
		||||
            for (auto const& book : value.as_array()) {
 | 
			
		||||
                if (!book.is_object())
 | 
			
		||||
                    return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
 | 
			
		||||
 | 
			
		||||
                if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
 | 
			
		||||
                    return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
 | 
			
		||||
 | 
			
		||||
                auto const parsedBook = parseBook(book.as_object());
 | 
			
		||||
                if (auto const status = std::get_if<Status>(&parsedBook))
 | 
			
		||||
                    return Error(*status);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return MaybeError{};
 | 
			
		||||
        }};
 | 
			
		||||
 | 
			
		||||
    static auto const rpcSpec = RpcSpec{
 | 
			
		||||
        {JS(streams), validation::SubscribeStreamValidator},
 | 
			
		||||
        {JS(accounts), validation::SubscribeAccountsValidator},
 | 
			
		||||
        {JS(accounts_proposed), validation::SubscribeAccountsValidator},
 | 
			
		||||
        {JS(books), booksValidator},
 | 
			
		||||
        {JS(url), check::Deprecated{}},
 | 
			
		||||
        {JS(rt_accounts), check::Deprecated{}},
 | 
			
		||||
        {"rt_transactions", check::Deprecated{}},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return rpcSpec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
UnsubscribeHandler::Result
 | 
			
		||||
UnsubscribeHandler::process(Input input, Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    if (input.streams)
 | 
			
		||||
        unsubscribeFromStreams(*(input.streams), ctx.session);
 | 
			
		||||
 | 
			
		||||
    if (input.accounts)
 | 
			
		||||
        unsubscribeFromAccounts(*(input.accounts), ctx.session);
 | 
			
		||||
 | 
			
		||||
    if (input.accountsProposed)
 | 
			
		||||
        unsubscribeFromProposedAccounts(*(input.accountsProposed), ctx.session);
 | 
			
		||||
 | 
			
		||||
    if (input.books)
 | 
			
		||||
        unsubscribeFromBooks(*(input.books), ctx.session);
 | 
			
		||||
 | 
			
		||||
    return Output{};
 | 
			
		||||
}
 | 
			
		||||
void
 | 
			
		||||
UnsubscribeHandler::unsubscribeFromStreams(
 | 
			
		||||
    std::vector<std::string> const& streams,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& stream : streams) {
 | 
			
		||||
        if (stream == "ledger") {
 | 
			
		||||
            subscriptions_->unsubLedger(session);
 | 
			
		||||
        } else if (stream == "transactions") {
 | 
			
		||||
            subscriptions_->unsubTransactions(session);
 | 
			
		||||
        } else if (stream == "transactions_proposed") {
 | 
			
		||||
            subscriptions_->unsubProposedTransactions(session);
 | 
			
		||||
        } else if (stream == "validations") {
 | 
			
		||||
            subscriptions_->unsubValidation(session);
 | 
			
		||||
        } else if (stream == "manifests") {
 | 
			
		||||
            subscriptions_->unsubManifest(session);
 | 
			
		||||
        } else if (stream == "book_changes") {
 | 
			
		||||
            subscriptions_->unsubBookChanges(session);
 | 
			
		||||
        } else {
 | 
			
		||||
            ASSERT(false, "Unknown stream: {}", stream);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
void
 | 
			
		||||
UnsubscribeHandler::unsubscribeFromAccounts(
 | 
			
		||||
    std::vector<std::string> accounts,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& account : accounts) {
 | 
			
		||||
        auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
        subscriptions_->unsubAccount(*accountID, session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
void
 | 
			
		||||
UnsubscribeHandler::unsubscribeFromProposedAccounts(
 | 
			
		||||
    std::vector<std::string> accountsProposed,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& account : accountsProposed) {
 | 
			
		||||
        auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
        subscriptions_->unsubProposedAccount(*accountID, session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
void
 | 
			
		||||
UnsubscribeHandler::unsubscribeFromBooks(
 | 
			
		||||
    std::vector<OrderBook> const& books,
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
) const
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& orderBook : books) {
 | 
			
		||||
        subscriptions_->unsubBook(orderBook.book, session);
 | 
			
		||||
 | 
			
		||||
        if (orderBook.both)
 | 
			
		||||
            subscriptions_->unsubBook(ripple::reversed(orderBook.book), session);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
UnsubscribeHandler::Input
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<UnsubscribeHandler::Input>, boost::json::value const& jv)
 | 
			
		||||
{
 | 
			
		||||
    auto input = UnsubscribeHandler::Input{};
 | 
			
		||||
    auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
    if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
 | 
			
		||||
        input.streams = std::vector<std::string>();
 | 
			
		||||
        for (auto const& stream : streams->value().as_array())
 | 
			
		||||
            input.streams->push_back(boost::json::value_to<std::string>(stream));
 | 
			
		||||
    }
 | 
			
		||||
    if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
 | 
			
		||||
        input.accounts = std::vector<std::string>();
 | 
			
		||||
        for (auto const& account : accounts->value().as_array())
 | 
			
		||||
            input.accounts->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
    }
 | 
			
		||||
    if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end()) {
 | 
			
		||||
        input.accountsProposed = std::vector<std::string>();
 | 
			
		||||
        for (auto const& account : accountsProposed->value().as_array())
 | 
			
		||||
            input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
    }
 | 
			
		||||
    if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
 | 
			
		||||
        input.books = std::vector<UnsubscribeHandler::OrderBook>();
 | 
			
		||||
        for (auto const& book : books->value().as_array()) {
 | 
			
		||||
            auto internalBook = UnsubscribeHandler::OrderBook{};
 | 
			
		||||
            auto const& bookObject = book.as_object();
 | 
			
		||||
 | 
			
		||||
            if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
 | 
			
		||||
                internalBook.both = both->value().as_bool();
 | 
			
		||||
 | 
			
		||||
            auto const parsedBookMaybe = parseBook(book.as_object());
 | 
			
		||||
            internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
            input.books->push_back(internalBook);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return input;
 | 
			
		||||
}
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
@@ -20,17 +20,13 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <ripple/protocol/Book.h>
 | 
			
		||||
#include <ripple/protocol/ErrorCodes.h>
 | 
			
		||||
#include <ripple/protocol/jss.h>
 | 
			
		||||
@@ -39,8 +35,6 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <variant>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace feed {
 | 
			
		||||
@@ -50,12 +44,16 @@ class SubscriptionManager;
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Handles the `unsubscribe` command which is used to disconnect a subscriber from a feed
 | 
			
		||||
 * @brief Handles the `unsubscribe` command which is used to disconnect a subscriber from a feed.
 | 
			
		||||
 * The unsubscribe command tells the server to stop sending messages for a particular subscription or set of
 | 
			
		||||
 * subscriptions.
 | 
			
		||||
 *
 | 
			
		||||
 * For more details see: https://xrpl.org/unsubscribe.html
 | 
			
		||||
 */
 | 
			
		||||
template <typename SubscriptionManagerType>
 | 
			
		||||
class BaseUnsubscribeHandler {
 | 
			
		||||
 | 
			
		||||
class UnsubscribeHandler {
 | 
			
		||||
    std::shared_ptr<BackendInterface> sharedPtrBackend_;
 | 
			
		||||
    std::shared_ptr<SubscriptionManagerType> subscriptions_;
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
@@ -85,13 +83,10 @@ public:
 | 
			
		||||
     * @param sharedPtrBackend The backend to use
 | 
			
		||||
     * @param subscriptions The subscription manager to use
 | 
			
		||||
     */
 | 
			
		||||
    BaseUnsubscribeHandler(
 | 
			
		||||
    UnsubscribeHandler(
 | 
			
		||||
        std::shared_ptr<BackendInterface> const& sharedPtrBackend,
 | 
			
		||||
        std::shared_ptr<SubscriptionManagerType> const& subscriptions
 | 
			
		||||
    )
 | 
			
		||||
        : sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Returns the API specification for the command
 | 
			
		||||
@@ -100,40 +95,7 @@ public:
 | 
			
		||||
     * @return The spec for the given apiVersion
 | 
			
		||||
     */
 | 
			
		||||
    static RpcSpecConstRef
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
    {
 | 
			
		||||
        static auto const booksValidator =
 | 
			
		||||
            validation::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"}};
 | 
			
		||||
 | 
			
		||||
                for (auto const& book : value.as_array()) {
 | 
			
		||||
                    if (!book.is_object())
 | 
			
		||||
                        return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
 | 
			
		||||
 | 
			
		||||
                    if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
 | 
			
		||||
                        return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
 | 
			
		||||
 | 
			
		||||
                    auto const parsedBook = parseBook(book.as_object());
 | 
			
		||||
                    if (auto const status = std::get_if<Status>(&parsedBook))
 | 
			
		||||
                        return Error(*status);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return MaybeError{};
 | 
			
		||||
            }};
 | 
			
		||||
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(streams), validation::SubscribeStreamValidator},
 | 
			
		||||
            {JS(accounts), validation::SubscribeAccountsValidator},
 | 
			
		||||
            {JS(accounts_proposed), validation::SubscribeAccountsValidator},
 | 
			
		||||
            {JS(books), booksValidator},
 | 
			
		||||
            {JS(url), check::Deprecated{}},
 | 
			
		||||
            {JS(rt_accounts), check::Deprecated{}},
 | 
			
		||||
            {"rt_transactions", check::Deprecated{}},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return rpcSpec;
 | 
			
		||||
    }
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Process the Unsubscribe command
 | 
			
		||||
@@ -143,127 +105,35 @@ public:
 | 
			
		||||
     * @return The result of the operation
 | 
			
		||||
     */
 | 
			
		||||
    Result
 | 
			
		||||
    process(Input input, Context const& ctx) const
 | 
			
		||||
    {
 | 
			
		||||
        if (input.streams)
 | 
			
		||||
            unsubscribeFromStreams(*(input.streams), ctx.session);
 | 
			
		||||
 | 
			
		||||
        if (input.accounts)
 | 
			
		||||
            unsubscribeFromAccounts(*(input.accounts), ctx.session);
 | 
			
		||||
 | 
			
		||||
        if (input.accountsProposed)
 | 
			
		||||
            unsubscribeFromProposedAccounts(*(input.accountsProposed), ctx.session);
 | 
			
		||||
 | 
			
		||||
        if (input.books)
 | 
			
		||||
            unsubscribeFromBooks(*(input.books), ctx.session);
 | 
			
		||||
 | 
			
		||||
        return Output{};
 | 
			
		||||
    }
 | 
			
		||||
    process(Input input, Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    unsubscribeFromStreams(std::vector<std::string> const& streams, std::shared_ptr<web::ConnectionBase> const& session)
 | 
			
		||||
        const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& stream : streams) {
 | 
			
		||||
            if (stream == "ledger") {
 | 
			
		||||
                subscriptions_->unsubLedger(session);
 | 
			
		||||
            } else if (stream == "transactions") {
 | 
			
		||||
                subscriptions_->unsubTransactions(session);
 | 
			
		||||
            } else if (stream == "transactions_proposed") {
 | 
			
		||||
                subscriptions_->unsubProposedTransactions(session);
 | 
			
		||||
            } else if (stream == "validations") {
 | 
			
		||||
                subscriptions_->unsubValidation(session);
 | 
			
		||||
            } else if (stream == "manifests") {
 | 
			
		||||
                subscriptions_->unsubManifest(session);
 | 
			
		||||
            } else if (stream == "book_changes") {
 | 
			
		||||
                subscriptions_->unsubBookChanges(session);
 | 
			
		||||
            } else {
 | 
			
		||||
                ASSERT(false, "Unknown stream: {}", stream);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    unsubscribeFromAccounts(std::vector<std::string> accounts, std::shared_ptr<web::ConnectionBase> const& session)
 | 
			
		||||
        const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& account : accounts) {
 | 
			
		||||
            auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
            subscriptions_->unsubAccount(*accountID, session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    unsubscribeFromProposedAccounts(
 | 
			
		||||
        std::vector<std::string> accountsProposed,
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& session
 | 
			
		||||
    ) const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& account : accountsProposed) {
 | 
			
		||||
            auto const accountID = accountFromStringStrict(account);
 | 
			
		||||
            subscriptions_->unsubProposedAccount(*accountID, session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    ) const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    unsubscribeFromBooks(std::vector<OrderBook> const& books, std::shared_ptr<web::ConnectionBase> const& session) const
 | 
			
		||||
    {
 | 
			
		||||
        for (auto const& orderBook : books) {
 | 
			
		||||
            subscriptions_->unsubBook(orderBook.book, session);
 | 
			
		||||
 | 
			
		||||
            if (orderBook.both)
 | 
			
		||||
                subscriptions_->unsubBook(ripple::reversed(orderBook.book), session);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    unsubscribeFromBooks(std::vector<OrderBook> const& books, std::shared_ptr<web::ConnectionBase> const& session)
 | 
			
		||||
        const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Convert a JSON object to an Input
 | 
			
		||||
     *
 | 
			
		||||
     * @param jv The JSON object to convert
 | 
			
		||||
     * @return The Input object
 | 
			
		||||
     */
 | 
			
		||||
    friend Input
 | 
			
		||||
    tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv)
 | 
			
		||||
    {
 | 
			
		||||
        auto input = Input{};
 | 
			
		||||
        auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
        if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
 | 
			
		||||
            input.streams = std::vector<std::string>();
 | 
			
		||||
            for (auto const& stream : streams->value().as_array())
 | 
			
		||||
                input.streams->push_back(boost::json::value_to<std::string>(stream));
 | 
			
		||||
        }
 | 
			
		||||
        if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
 | 
			
		||||
            input.accounts = std::vector<std::string>();
 | 
			
		||||
            for (auto const& account : accounts->value().as_array())
 | 
			
		||||
                input.accounts->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
        }
 | 
			
		||||
        if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed));
 | 
			
		||||
            accountsProposed != jsonObject.end()) {
 | 
			
		||||
            input.accountsProposed = std::vector<std::string>();
 | 
			
		||||
            for (auto const& account : accountsProposed->value().as_array())
 | 
			
		||||
                input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
 | 
			
		||||
        }
 | 
			
		||||
        if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
 | 
			
		||||
            input.books = std::vector<OrderBook>();
 | 
			
		||||
            for (auto const& book : books->value().as_array()) {
 | 
			
		||||
                auto internalBook = OrderBook{};
 | 
			
		||||
                auto const& bookObject = book.as_object();
 | 
			
		||||
 | 
			
		||||
                if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
 | 
			
		||||
                    internalBook.both = both->value().as_bool();
 | 
			
		||||
 | 
			
		||||
                auto const parsedBookMaybe = parseBook(book.as_object());
 | 
			
		||||
                internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
                input.books->push_back(internalBook);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return input;
 | 
			
		||||
    }
 | 
			
		||||
    tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The unsubscribe command tells the server to stop sending messages for a particular subscription or set of
 | 
			
		||||
 * subscriptions.
 | 
			
		||||
 *
 | 
			
		||||
 * For more details see: https://xrpl.org/unsubscribe.html
 | 
			
		||||
 */
 | 
			
		||||
using UnsubscribeHandler = BaseUnsubscribeHandler<feed::SubscriptionManager>;
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
 
 | 
			
		||||
@@ -16,13 +16,21 @@
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/Random.hpp"
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <random>
 | 
			
		||||
 | 
			
		||||
namespace util {
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
Random::setSeed(size_t seed)
 | 
			
		||||
{
 | 
			
		||||
    generator_.seed(seed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::mt19937_64 Random::generator_{std::chrono::system_clock::now().time_since_epoch().count()};
 | 
			
		||||
 | 
			
		||||
}  // namespace util
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <random>
 | 
			
		||||
 | 
			
		||||
namespace util {
 | 
			
		||||
@@ -50,6 +51,14 @@ public:
 | 
			
		||||
        return distribution(generator_);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the seed for the random number generator
 | 
			
		||||
     *
 | 
			
		||||
     * @param seed Seed to set
 | 
			
		||||
     */
 | 
			
		||||
    static void
 | 
			
		||||
    setSeed(size_t seed);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static std::mt19937_64 generator_;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
 | 
			
		||||
// Base class for feed tests, providing easy way to access the received feed
 | 
			
		||||
template <typename TestedFeed>
 | 
			
		||||
class FeedBaseTest : public util::prometheus::WithPrometheus, public SyncAsioContextTest, public MockBackendTest {
 | 
			
		||||
struct FeedBaseTest : util::prometheus::WithPrometheus, SyncAsioContextTest, MockBackendTest {
 | 
			
		||||
protected:
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> sessionPtr;
 | 
			
		||||
    std::shared_ptr<TestedFeed> testFeedPtr;
 | 
			
		||||
@@ -44,7 +44,6 @@ protected:
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        SyncAsioContextTest::SetUp();
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
        testFeedPtr = std::make_shared<TestedFeed>(ctx);
 | 
			
		||||
        sessionPtr = std::make_shared<MockSession>();
 | 
			
		||||
        sessionPtr->apiSubVersion = 1;
 | 
			
		||||
@@ -56,7 +55,6 @@ protected:
 | 
			
		||||
    {
 | 
			
		||||
        sessionPtr.reset();
 | 
			
		||||
        testFeedPtr.reset();
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
        SyncAsioContextTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -195,23 +195,8 @@ protected:
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType = ::testing::NiceMock>
 | 
			
		||||
struct MockBackendTestBase : virtual public NoLoggerFixture {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        backend.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class BackendProxy {
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend;
 | 
			
		||||
 | 
			
		||||
    private:
 | 
			
		||||
        void
 | 
			
		||||
        reset()
 | 
			
		||||
        {
 | 
			
		||||
            backend = std::make_shared<MockType<MockBackend>>(util::Config{});
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        friend MockBackendTestBase;
 | 
			
		||||
        std::shared_ptr<MockType<MockBackend>> backend = std::make_shared<MockType<MockBackend>>(util::Config{});
 | 
			
		||||
 | 
			
		||||
    public:
 | 
			
		||||
        auto
 | 
			
		||||
@@ -230,11 +215,10 @@ struct MockBackendTestBase : virtual public NoLoggerFixture {
 | 
			
		||||
            return backend;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        operator MockBackend*()
 | 
			
		||||
        MockType<MockBackend>&
 | 
			
		||||
        operator*()
 | 
			
		||||
        {
 | 
			
		||||
            MockBackend* ret = dynamic_cast<MockBackend*>(backend.get());
 | 
			
		||||
            [&] { ASSERT_NE(ret, nullptr); }();
 | 
			
		||||
            return ret;
 | 
			
		||||
            return *backend;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -266,14 +250,6 @@ using MockBackendTestNaggy = MockBackendTestBase<::testing::NaggyMock>;
 | 
			
		||||
 */
 | 
			
		||||
using MockBackendTestStrict = MockBackendTestBase<::testing::StrictMock>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Fixture with a mock subscription manager
 | 
			
		||||
 */
 | 
			
		||||
struct MockSubscriptionManagerTest : virtual public NoLoggerFixture {
 | 
			
		||||
protected:
 | 
			
		||||
    std::shared_ptr<MockSubscriptionManager> mockSubscriptionManagerPtr = std::make_shared<MockSubscriptionManager>();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Fixture with a mock etl balancer
 | 
			
		||||
 */
 | 
			
		||||
@@ -283,7 +259,7 @@ protected:
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Fixture with a mock subscription manager
 | 
			
		||||
 * @brief Fixture with a mock etl service
 | 
			
		||||
 */
 | 
			
		||||
template <template <typename> typename MockType = ::testing::NiceMock>
 | 
			
		||||
struct MockETLServiceTestBase : virtual public NoLoggerFixture {
 | 
			
		||||
@@ -328,15 +304,14 @@ protected:
 | 
			
		||||
 * HandlerBaseTestStrict.
 | 
			
		||||
 */
 | 
			
		||||
template <template <typename> typename MockType = ::testing::NiceMock>
 | 
			
		||||
struct HandlerBaseTestBase : public MockBackendTestBase<MockType>,
 | 
			
		||||
                             public util::prometheus::WithPrometheus,
 | 
			
		||||
                             public SyncAsioContextTest,
 | 
			
		||||
                             public MockETLServiceTestBase<MockType> {
 | 
			
		||||
struct HandlerBaseTestBase : util::prometheus::WithPrometheus,
 | 
			
		||||
                             MockBackendTestBase<MockType>,
 | 
			
		||||
                             SyncAsioContextTest,
 | 
			
		||||
                             MockETLServiceTestBase<MockType> {
 | 
			
		||||
protected:
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTestBase<MockType>::SetUp();
 | 
			
		||||
        SyncAsioContextTest::SetUp();
 | 
			
		||||
        MockETLServiceTestBase<MockType>::SetUp();
 | 
			
		||||
    }
 | 
			
		||||
@@ -346,7 +321,6 @@ protected:
 | 
			
		||||
    {
 | 
			
		||||
        MockETLServiceTestBase<MockType>::TearDown();
 | 
			
		||||
        SyncAsioContextTest::TearDown();
 | 
			
		||||
        MockBackendTestBase<MockType>::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,13 +19,36 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
struct MockNetworkValidatedLedgers {
 | 
			
		||||
    MOCK_METHOD(void, push, (uint32_t), ());
 | 
			
		||||
    MOCK_METHOD(std::optional<uint32_t>, getMostRecent, (), ());
 | 
			
		||||
    MOCK_METHOD(bool, waitUntilValidatedByNetwork, (uint32_t), ());
 | 
			
		||||
struct MockNetworkValidatedLedgers : public etl::NetworkValidatedLedgersInterface {
 | 
			
		||||
    MOCK_METHOD(void, push, (uint32_t), (override));
 | 
			
		||||
    MOCK_METHOD(std::optional<uint32_t>, getMostRecent, (), (override));
 | 
			
		||||
    MOCK_METHOD(bool, waitUntilValidatedByNetwork, (uint32_t, std::optional<uint32_t>), (override));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType>
 | 
			
		||||
struct MockNetworkValidatedLedgersPtrImpl {
 | 
			
		||||
    std::shared_ptr<MockType<MockNetworkValidatedLedgers>> ptr =
 | 
			
		||||
        std::make_shared<MockType<MockNetworkValidatedLedgers>>();
 | 
			
		||||
 | 
			
		||||
    operator std::shared_ptr<etl::NetworkValidatedLedgersInterface>() const
 | 
			
		||||
    {
 | 
			
		||||
        return ptr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MockType<MockNetworkValidatedLedgers>&
 | 
			
		||||
    operator*()
 | 
			
		||||
    {
 | 
			
		||||
        return *ptr;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using MockNetworkValidatedLedgersPtr = MockNetworkValidatedLedgersPtrImpl<testing::NiceMock>;
 | 
			
		||||
using StrictMockNetworkValidatedLedgersPtr = MockNetworkValidatedLedgersPtrImpl<testing::StrictMock>;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,41 +18,180 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "etl/ETLHelpers.hpp"
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/uuid/uuid.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <grpcpp/support/status.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
class MockSource {
 | 
			
		||||
public:
 | 
			
		||||
    MOCK_METHOD(bool, isConnected, (), (const));
 | 
			
		||||
    MOCK_METHOD(boost::json::object, toJson, (), (const));
 | 
			
		||||
    MOCK_METHOD(void, run, ());
 | 
			
		||||
    MOCK_METHOD(void, pause, ());
 | 
			
		||||
    MOCK_METHOD(void, resume, ());
 | 
			
		||||
    MOCK_METHOD(std::string, toString, (), (const));
 | 
			
		||||
    MOCK_METHOD(bool, hasLedger, (uint32_t), (const));
 | 
			
		||||
    MOCK_METHOD((std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>), fetchLedger, (uint32_t, bool, bool));
 | 
			
		||||
    MOCK_METHOD((std::pair<std::vector<std::string>, bool>), loadInitialLedger, (uint32_t, uint32_t, bool));
 | 
			
		||||
struct MockSource : etl::SourceBase {
 | 
			
		||||
    MOCK_METHOD(void, run, (), (override));
 | 
			
		||||
    MOCK_METHOD(bool, isConnected, (), (const, override));
 | 
			
		||||
    MOCK_METHOD(void, setForwarding, (bool), (override));
 | 
			
		||||
    MOCK_METHOD(boost::json::object, toJson, (), (const, override));
 | 
			
		||||
    MOCK_METHOD(std::string, toString, (), (const, override));
 | 
			
		||||
    MOCK_METHOD(bool, hasLedger, (uint32_t), (const, override));
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        (std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>),
 | 
			
		||||
        fetchLedger,
 | 
			
		||||
        (uint32_t, bool, bool),
 | 
			
		||||
        (override)
 | 
			
		||||
    );
 | 
			
		||||
    MOCK_METHOD((std::pair<std::vector<std::string>, bool>), loadInitialLedger, (uint32_t, uint32_t, bool), (override));
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        std::optional<boost::json::object>,
 | 
			
		||||
        forwardToRippled,
 | 
			
		||||
        (boost::json::object const&, std::optional<std::string> const&, boost::asio::yield_context),
 | 
			
		||||
        (const)
 | 
			
		||||
        (const, override)
 | 
			
		||||
    );
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        std::optional<boost::json::object>,
 | 
			
		||||
        requestFromRippled,
 | 
			
		||||
        (boost::json::object const&, std::optional<std::string> const&, boost::asio::yield_context),
 | 
			
		||||
        (const)
 | 
			
		||||
    );
 | 
			
		||||
    MOCK_METHOD(boost::uuids::uuid, token, (), (const));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType>
 | 
			
		||||
using MockSourcePtr = std::shared_ptr<MockType<MockSource>>;
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType>
 | 
			
		||||
class MockSourceWrapper : public etl::SourceBase {
 | 
			
		||||
    MockSourcePtr<MockType> mock_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    MockSourceWrapper(MockSourcePtr<MockType> mockData) : mock_(std::move(mockData))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    run() override
 | 
			
		||||
    {
 | 
			
		||||
        mock_->run();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool
 | 
			
		||||
    isConnected() const override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->isConnected();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    setForwarding(bool isForwarding) override
 | 
			
		||||
    {
 | 
			
		||||
        mock_->setForwarding(isForwarding);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    toJson() const override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->toJson();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::string
 | 
			
		||||
    toString() const override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool
 | 
			
		||||
    hasLedger(uint32_t sequence) const override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->hasLedger(sequence);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse>
 | 
			
		||||
    fetchLedger(uint32_t sequence, bool getObjects, bool getObjectNeighbors) override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->fetchLedger(sequence, getObjects, getObjectNeighbors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::pair<std::vector<std::string>, bool>
 | 
			
		||||
    loadInitialLedger(uint32_t sequence, uint32_t maxLedger, bool getObjects) override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->loadInitialLedger(sequence, maxLedger, getObjects);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::optional<boost::json::object>
 | 
			
		||||
    forwardToRippled(
 | 
			
		||||
        boost::json::object const& request,
 | 
			
		||||
        std::optional<std::string> const& forwardToRippledClientIp,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    ) const override
 | 
			
		||||
    {
 | 
			
		||||
        return mock_->forwardToRippled(request, forwardToRippledClientIp, yield);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct MockSourceCallbacks {
 | 
			
		||||
    etl::SourceBase::OnDisconnectHook onDisconnect;
 | 
			
		||||
    etl::SourceBase::OnConnectHook onConnect;
 | 
			
		||||
    etl::SourceBase::OnLedgerClosedHook onLedgerClosed;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType>
 | 
			
		||||
struct MockSourceData {
 | 
			
		||||
    MockSourcePtr<MockType> source = std::make_shared<MockType<MockSource>>();
 | 
			
		||||
    std::optional<MockSourceCallbacks> callbacks;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType = testing::NiceMock>
 | 
			
		||||
class MockSourceFactoryImpl {
 | 
			
		||||
    std::vector<MockSourceData<MockType>> mockData_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    MockSourceFactoryImpl(size_t numSources)
 | 
			
		||||
    {
 | 
			
		||||
        mockData_.reserve(numSources);
 | 
			
		||||
        std::ranges::generate_n(std::back_inserter(mockData_), numSources, [] { return MockSourceData<MockType>{}; });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    etl::SourcePtr
 | 
			
		||||
    makeSourceMock(
 | 
			
		||||
        util::Config const&,
 | 
			
		||||
        boost::asio::io_context&,
 | 
			
		||||
        std::shared_ptr<BackendInterface>,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManagerInterface>,
 | 
			
		||||
        std::shared_ptr<etl::NetworkValidatedLedgersInterface>,
 | 
			
		||||
        etl::SourceBase::OnConnectHook onConnect,
 | 
			
		||||
        etl::SourceBase::OnDisconnectHook onDisconnect,
 | 
			
		||||
        etl::SourceBase::OnLedgerClosedHook onLedgerClosed
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        auto it = std::ranges::find_if(mockData_, [](auto const& d) { return not d.callbacks.has_value(); });
 | 
			
		||||
        [&]() { ASSERT_NE(it, mockData_.end()) << "Make source called more than expected"; }();
 | 
			
		||||
        it->callbacks = MockSourceCallbacks{std::move(onDisconnect), std::move(onConnect), std::move(onLedgerClosed)};
 | 
			
		||||
 | 
			
		||||
        return std::make_unique<MockSourceWrapper<MockType>>(it->source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MockType<MockSource>&
 | 
			
		||||
    sourceAt(size_t index)
 | 
			
		||||
    {
 | 
			
		||||
        return *mockData_.at(index).source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MockSourceCallbacks&
 | 
			
		||||
    callbacksAt(size_t index)
 | 
			
		||||
    {
 | 
			
		||||
        auto& callbacks = mockData_.at(index).callbacks;
 | 
			
		||||
        [&]() { ASSERT_TRUE(callbacks.has_value()) << "Callbacks not set"; }();
 | 
			
		||||
        return *callbacks;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using MockSourceFactory = MockSourceFactoryImpl<>;
 | 
			
		||||
using StrictMockSourceFactory = MockSourceFactoryImpl<testing::StrictMock>;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/Types.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "feed/Types.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json.hpp>
 | 
			
		||||
@@ -36,70 +37,89 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
struct MockSubscriptionManager {
 | 
			
		||||
public:
 | 
			
		||||
    using session_ptr = std::shared_ptr<web::ConnectionBase>;
 | 
			
		||||
    MockSubscriptionManager() = default;
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(boost::json::object, subLedger, (boost::asio::yield_context, session_ptr), ());
 | 
			
		||||
struct MockSubscriptionManager : feed::SubscriptionManagerInterface {
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        boost::json::object,
 | 
			
		||||
        subLedger,
 | 
			
		||||
        (boost::asio::yield_context, feed::SubscriberSharedPtr const&),
 | 
			
		||||
        (override)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        void,
 | 
			
		||||
        pubLedger,
 | 
			
		||||
        (ripple::LedgerInfo const&, ripple::Fees const&, std::string const&, std::uint32_t),
 | 
			
		||||
        ()
 | 
			
		||||
        (ripple::LedgerHeader const&, ripple::Fees const&, std::string const&, std::uint32_t),
 | 
			
		||||
        (const, override)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        void,
 | 
			
		||||
        pubBookChanges,
 | 
			
		||||
        (ripple::LedgerInfo const&, std::vector<data::TransactionAndMetadata> const&),
 | 
			
		||||
        ()
 | 
			
		||||
        (const, override)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubLedger, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubLedger, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subTransactions, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subTransactions, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubTransactions, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubTransactions, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, pubTransaction, (data::TransactionAndMetadata const&, ripple::LedgerInfo const&), ());
 | 
			
		||||
    MOCK_METHOD(void, pubTransaction, (data::TransactionAndMetadata const&, ripple::LedgerInfo const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subAccount, (ripple::AccountID const&, session_ptr&), ());
 | 
			
		||||
    MOCK_METHOD(void, subAccount, (ripple::AccountID const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubAccount, (ripple::AccountID const&, session_ptr const&), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubAccount, (ripple::AccountID const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subBook, (ripple::Book const&, session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subBook, (ripple::Book const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubBook, (ripple::Book const&, session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubBook, (ripple::Book const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subBookChanges, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subBookChanges, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubBookChanges, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubBookChanges, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subManifest, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subManifest, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubManifest, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubManifest, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subValidation, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subValidation, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubValidation, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubValidation, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, forwardProposedTransaction, (boost::json::object const&), ());
 | 
			
		||||
    MOCK_METHOD(void, forwardProposedTransaction, (boost::json::object const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, forwardManifest, (boost::json::object const&), ());
 | 
			
		||||
    MOCK_METHOD(void, forwardManifest, (boost::json::object const&), (const, override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, forwardValidation, (boost::json::object const&), ());
 | 
			
		||||
    MOCK_METHOD(void, forwardValidation, (boost::json::object const&), (const, override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subProposedAccount, (ripple::AccountID const&, session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subProposedAccount, (ripple::AccountID const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubProposedAccount, (ripple::AccountID const&, session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubProposedAccount, (ripple::AccountID const&, feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, subProposedTransactions, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, subProposedTransactions, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, unsubProposedTransactions, (session_ptr), ());
 | 
			
		||||
    MOCK_METHOD(void, unsubProposedTransactions, (feed::SubscriberSharedPtr const&), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, cleanup, (session_ptr), ());
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(boost::json::object, report, (), (const));
 | 
			
		||||
    MOCK_METHOD(boost::json::object, report, (), (const, override));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <template <typename> typename MockType = ::testing::NiceMock>
 | 
			
		||||
struct MockSubscriptionManagerSharedPtrImpl {
 | 
			
		||||
    std::shared_ptr<MockType<MockSubscriptionManager>> subscriptionManagerMock =
 | 
			
		||||
        std::make_shared<MockType<MockSubscriptionManager>>();
 | 
			
		||||
 | 
			
		||||
    operator std::shared_ptr<feed::SubscriptionManagerInterface>()
 | 
			
		||||
    {
 | 
			
		||||
        return subscriptionManagerMock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MockType<MockSubscriptionManager>&
 | 
			
		||||
    operator*()
 | 
			
		||||
    {
 | 
			
		||||
        return *subscriptionManagerMock;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using MockSubscriptionManagerSharedPtr = MockSubscriptionManagerSharedPtrImpl<>;
 | 
			
		||||
using StrictMockSubscriptionManagerSharedPtr = MockSubscriptionManagerSharedPtrImpl<testing::StrictMock>;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,8 @@ target_sources(
 | 
			
		||||
          etl/ForwardingSourceTests.cpp
 | 
			
		||||
          etl/GrpcSourceTests.cpp
 | 
			
		||||
          etl/LedgerPublisherTests.cpp
 | 
			
		||||
          etl/SourceTests.cpp
 | 
			
		||||
          etl/SubscriptionSourceDependenciesTests.cpp
 | 
			
		||||
          etl/LoadBalancerTests.cpp
 | 
			
		||||
          etl/SourceImplTests.cpp
 | 
			
		||||
          etl/SubscriptionSourceTests.cpp
 | 
			
		||||
          etl/TransformerTests.cpp
 | 
			
		||||
          # Feed
 | 
			
		||||
@@ -114,6 +114,7 @@ target_sources(
 | 
			
		||||
          util/requests/RequestBuilderTests.cpp
 | 
			
		||||
          util/requests/SslContextTests.cpp
 | 
			
		||||
          util/requests/WsConnectionTests.cpp
 | 
			
		||||
          util/RandomTests.cpp
 | 
			
		||||
          util/RetryTests.cpp
 | 
			
		||||
          util/SignalsHandlerTests.cpp
 | 
			
		||||
          util/TxUtilTests.cpp
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ using namespace testing;
 | 
			
		||||
constexpr static auto MAXSEQ = 30;
 | 
			
		||||
constexpr static auto MINSEQ = 10;
 | 
			
		||||
 | 
			
		||||
struct BackendInterfaceTest : MockBackendTestNaggy, SyncAsioContextTest, WithPrometheus {};
 | 
			
		||||
struct BackendInterfaceTest : WithPrometheus, MockBackendTestNaggy, SyncAsioContextTest {};
 | 
			
		||||
 | 
			
		||||
TEST_F(BackendInterfaceTest, FetchFeesSuccessPath)
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -45,19 +45,6 @@ namespace {
 | 
			
		||||
constexpr auto SEQ = 30;
 | 
			
		||||
 | 
			
		||||
struct CacheLoaderTest : util::prometheus::WithPrometheus, MockBackendTest {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    DiffProvider diffProvider;
 | 
			
		||||
    MockCache cache;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -76,3 +76,15 @@ TEST_F(ETLStateTest, NetworkIdInvalid)
 | 
			
		||||
    ASSERT_TRUE(state.has_value());
 | 
			
		||||
    EXPECT_FALSE(state->networkID.has_value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLStateTest, ResponseHasError)
 | 
			
		||||
{
 | 
			
		||||
    auto const json = json::parse(
 | 
			
		||||
        R"JSON({
 | 
			
		||||
            "error": "error"
 | 
			
		||||
        })JSON"
 | 
			
		||||
    );
 | 
			
		||||
    EXPECT_CALL(source, forwardToRippled).WillOnce(Return(json.as_object()));
 | 
			
		||||
    auto const state = etl::ETLState::fetchETLStateFromSource(source);
 | 
			
		||||
    EXPECT_FALSE(state.has_value());
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,6 @@
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
using namespace testing;
 | 
			
		||||
@@ -38,127 +37,96 @@ using namespace etl;
 | 
			
		||||
struct ETLExtractorTest : util::prometheus::WithPrometheus, NoLoggerFixture {
 | 
			
		||||
    using ExtractionDataPipeType = MockExtractionDataPipe;
 | 
			
		||||
    using LedgerFetcherType = MockLedgerFetcher;
 | 
			
		||||
    using ExtractorType = etl::impl::Extractor<ExtractionDataPipeType, MockNetworkValidatedLedgers, LedgerFetcherType>;
 | 
			
		||||
    using ExtractorType = etl::impl::Extractor<ExtractionDataPipeType, LedgerFetcherType>;
 | 
			
		||||
 | 
			
		||||
    ExtractionDataPipeType dataPipe_;
 | 
			
		||||
    std::shared_ptr<MockNetworkValidatedLedgers> networkValidatedLedgers_ =
 | 
			
		||||
        std::make_shared<MockNetworkValidatedLedgers>();
 | 
			
		||||
    MockNetworkValidatedLedgersPtr networkValidatedLedgers_;
 | 
			
		||||
    LedgerFetcherType ledgerFetcher_;
 | 
			
		||||
    SystemState state_;
 | 
			
		||||
 | 
			
		||||
    std::unique_ptr<ExtractorType> extractor_;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    ETLExtractorTest()
 | 
			
		||||
    {
 | 
			
		||||
        state_.isStopping = false;
 | 
			
		||||
        state_.writeConflict = false;
 | 
			
		||||
        state_.isReadOnly = false;
 | 
			
		||||
        state_.isWriting = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        extractor_.reset();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, StopsWhenCurrentSequenceExceedsFinishSequence)
 | 
			
		||||
{
 | 
			
		||||
    auto const rawNetworkValidatedLedgersPtr = networkValidatedLedgers_.get();
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).WillByDefault(Return(true));
 | 
			
		||||
    EXPECT_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).Times(3);
 | 
			
		||||
    ON_CALL(dataPipe_, getStride).WillByDefault(Return(4));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, getStride).Times(3);
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, waitUntilValidatedByNetwork).Times(3).WillRepeatedly(Return(true));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, getStride).Times(3).WillRepeatedly(Return(4));
 | 
			
		||||
 | 
			
		||||
    auto response = FakeFetchResponse{};
 | 
			
		||||
    ON_CALL(ledgerFetcher_, fetchDataAndDiff(_)).WillByDefault(Return(response));
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).Times(3);
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).Times(3).WillRepeatedly(Return(response));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, push).Times(3);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
 | 
			
		||||
    // expected to invoke for seq 0, 4, 8 and finally stop as seq will be greater than finishing seq
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 11, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 11, state_};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, StopsOnWriteConflict)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0));
 | 
			
		||||
    state_.writeConflict = true;
 | 
			
		||||
 | 
			
		||||
    // despite finish sequence being far ahead, we set writeConflict and so exit the loop immediately
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, StopsOnServerShutdown)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0));
 | 
			
		||||
    state_.isStopping = true;
 | 
			
		||||
 | 
			
		||||
    // despite finish sequence being far ahead, we set isStopping and so exit the loop immediately
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// stop extractor thread if fetcheResponse is empty
 | 
			
		||||
TEST_F(ETLExtractorTest, StopsIfFetchIsUnsuccessful)
 | 
			
		||||
{
 | 
			
		||||
    auto const rawNetworkValidatedLedgersPtr = networkValidatedLedgers_.get();
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, waitUntilValidatedByNetwork).WillOnce(Return(true));
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).WillByDefault(Return(true));
 | 
			
		||||
    EXPECT_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).Times(1);
 | 
			
		||||
 | 
			
		||||
    ON_CALL(ledgerFetcher_, fetchDataAndDiff(_)).WillByDefault(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).Times(1);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0));
 | 
			
		||||
 | 
			
		||||
    // we break immediately because fetchDataAndDiff returns nullopt
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, StopsIfWaitingUntilValidatedByNetworkTimesOut)
 | 
			
		||||
{
 | 
			
		||||
    auto const rawNetworkValidatedLedgersPtr = networkValidatedLedgers_.get();
 | 
			
		||||
 | 
			
		||||
    // note that in actual clio code we don't return false unless a timeout is specified and exceeded
 | 
			
		||||
    ON_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).WillByDefault(Return(false));
 | 
			
		||||
    EXPECT_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).Times(1);
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, waitUntilValidatedByNetwork).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
 | 
			
		||||
    // we emulate waitUntilValidatedByNetwork timing out which would lead to shutdown of the extractor thread
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 64, state_};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, SendsCorrectResponseToDataPipe)
 | 
			
		||||
{
 | 
			
		||||
    auto const rawNetworkValidatedLedgersPtr = networkValidatedLedgers_.get();
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).WillByDefault(Return(true));
 | 
			
		||||
    EXPECT_CALL(*rawNetworkValidatedLedgersPtr, waitUntilValidatedByNetwork).Times(1);
 | 
			
		||||
    ON_CALL(dataPipe_, getStride).WillByDefault(Return(4));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, getStride).Times(1);
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, waitUntilValidatedByNetwork).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, getStride).WillOnce(Return(4));
 | 
			
		||||
 | 
			
		||||
    auto response = FakeFetchResponse{1234};
 | 
			
		||||
    auto optionalResponse = std::optional<FakeFetchResponse>{};
 | 
			
		||||
 | 
			
		||||
    ON_CALL(ledgerFetcher_, fetchDataAndDiff(_)).WillByDefault(Return(response));
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).Times(1);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, push).Times(1).WillOnce(SaveArg<1>(&optionalResponse));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0)).Times(1);
 | 
			
		||||
    EXPECT_CALL(ledgerFetcher_, fetchDataAndDiff).WillOnce(Return(response));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, push(_, std::optional{response}));
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(0));
 | 
			
		||||
 | 
			
		||||
    // expect to finish after just one response due to finishSequence set to 1
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 1, state_);
 | 
			
		||||
    extractor_->waitTillFinished();  // this is what clio does too. waiting for the thread to join
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(optionalResponse.has_value());
 | 
			
		||||
    EXPECT_EQ(optionalResponse.value(), response);
 | 
			
		||||
    ExtractorType extractor{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 0, 1, state_};
 | 
			
		||||
    extractor.waitTillFinished();  // this is what clio does too. waiting for the thread to join
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLExtractorTest, CallsPipeFinishWithInitialSequenceAtExit)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(123)).Times(1);
 | 
			
		||||
    EXPECT_CALL(dataPipe_, finish(123));
 | 
			
		||||
    state_.isStopping = true;
 | 
			
		||||
 | 
			
		||||
    extractor_ = std::make_unique<ExtractorType>(dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 123, 234, state_);
 | 
			
		||||
    ExtractorType{dataPipe_, networkValidatedLedgers_, ledgerFetcher_, 123, 234, state_};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,29 +50,22 @@ static auto constexpr LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A
 | 
			
		||||
static auto constexpr SEQ = 30;
 | 
			
		||||
static auto constexpr AGE = 800;
 | 
			
		||||
 | 
			
		||||
struct ETLLedgerPublisherTest : util::prometheus::WithPrometheus,
 | 
			
		||||
                                MockBackendTest,
 | 
			
		||||
                                SyncAsioContextTest,
 | 
			
		||||
                                MockSubscriptionManagerTest {
 | 
			
		||||
struct ETLLedgerPublisherTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
        SyncAsioContextTest::SetUp();
 | 
			
		||||
        MockSubscriptionManagerTest::SetUp();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockSubscriptionManagerTest::TearDown();
 | 
			
		||||
        SyncAsioContextTest::TearDown();
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    util::Config cfg{json::parse("{}")};
 | 
			
		||||
    MockCache mockCache;
 | 
			
		||||
    StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoIsWritingFalse)
 | 
			
		||||
@@ -83,14 +76,13 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoIsWritingFalse)
 | 
			
		||||
    impl::LedgerPublisher publisher(ctx, backend, mockCache, mockSubscriptionManagerPtr, dummyState);
 | 
			
		||||
    publisher.publish(dummyLedgerInfo);
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*backend, fetchLedgerDiff(SEQ, _)).WillByDefault(Return(std::vector<LedgerObject>{}));
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(SEQ, _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(SEQ, _)).WillOnce(Return(std::vector<LedgerObject>{}));
 | 
			
		||||
 | 
			
		||||
    // setLastPublishedSequence not in strand, should verify before run
 | 
			
		||||
    EXPECT_TRUE(publisher.getLastPublishedSequence());
 | 
			
		||||
    EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(mockCache, updateImp).Times(1);
 | 
			
		||||
    EXPECT_CALL(mockCache, updateImp);
 | 
			
		||||
 | 
			
		||||
    ctx.run();
 | 
			
		||||
    EXPECT_TRUE(backend->fetchLedgerRange());
 | 
			
		||||
@@ -106,8 +98,6 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoIsWritingTrue)
 | 
			
		||||
    impl::LedgerPublisher publisher(ctx, backend, mockCache, mockSubscriptionManagerPtr, dummyState);
 | 
			
		||||
    publisher.publish(dummyLedgerInfo);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(_, _)).Times(0);
 | 
			
		||||
 | 
			
		||||
    // setLastPublishedSequence not in strand, should verify before run
 | 
			
		||||
    EXPECT_TRUE(publisher.getLastPublishedSequence());
 | 
			
		||||
    EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
 | 
			
		||||
@@ -127,33 +117,26 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoInRange)
 | 
			
		||||
 | 
			
		||||
    publisher.publish(dummyLedgerInfo);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(_, _)).Times(0);
 | 
			
		||||
 | 
			
		||||
    // mock fetch fee
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
 | 
			
		||||
    ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillOnce(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger).Times(1);
 | 
			
		||||
    TransactionAndMetadata t1;
 | 
			
		||||
    t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
 | 
			
		||||
    t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData();
 | 
			
		||||
    t1.ledgerSequence = SEQ;
 | 
			
		||||
    ON_CALL(*backend, fetchAllTransactionsInLedger(SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(std::vector<TransactionAndMetadata>{t1}));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger).WillOnce(Return(std::vector<TransactionAndMetadata>{t1}));
 | 
			
		||||
 | 
			
		||||
    // setLastPublishedSequence not in strand, should verify before run
 | 
			
		||||
    EXPECT_TRUE(publisher.getLastPublishedSequence());
 | 
			
		||||
    EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr =
 | 
			
		||||
        dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubBookChanges);
 | 
			
		||||
    // mock 1 transaction
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubTransaction);
 | 
			
		||||
 | 
			
		||||
    ctx.run();
 | 
			
		||||
    // last publish time should be set
 | 
			
		||||
@@ -175,33 +158,27 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoCloseTimeGreaterThanNow)
 | 
			
		||||
    impl::LedgerPublisher publisher(ctx, backend, mockCache, mockSubscriptionManagerPtr, dummyState);
 | 
			
		||||
    publisher.publish(dummyLedgerInfo);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(_, _)).Times(0);
 | 
			
		||||
 | 
			
		||||
    // mock fetch fee
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
 | 
			
		||||
    ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillOnce(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger).Times(1);
 | 
			
		||||
    TransactionAndMetadata t1;
 | 
			
		||||
    t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
 | 
			
		||||
    t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData();
 | 
			
		||||
    t1.ledgerSequence = SEQ;
 | 
			
		||||
    ON_CALL(*backend, fetchAllTransactionsInLedger(SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(std::vector<TransactionAndMetadata>{t1}));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger(SEQ, _))
 | 
			
		||||
        .WillOnce(Return(std::vector<TransactionAndMetadata>{t1}));
 | 
			
		||||
 | 
			
		||||
    // setLastPublishedSequence not in strand, should verify before run
 | 
			
		||||
    EXPECT_TRUE(publisher.getLastPublishedSequence());
 | 
			
		||||
    EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr =
 | 
			
		||||
        dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubBookChanges);
 | 
			
		||||
    // mock 1 transaction
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubTransaction);
 | 
			
		||||
 | 
			
		||||
    ctx.run();
 | 
			
		||||
    // last publish time should be set
 | 
			
		||||
@@ -224,11 +201,10 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqMaxAttampt)
 | 
			
		||||
 | 
			
		||||
    static auto constexpr MAX_ATTEMPT = 2;
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend, hardFetchLedgerRange).Times(MAX_ATTEMPT);
 | 
			
		||||
 | 
			
		||||
    LedgerRange const range{.minSequence = SEQ - 1, .maxSequence = SEQ - 1};
 | 
			
		||||
    ON_CALL(*backend, hardFetchLedgerRange(_)).WillByDefault(Return(range));
 | 
			
		||||
    EXPECT_FALSE(publisher.publish(SEQ, MAX_ATTEMPT));
 | 
			
		||||
    EXPECT_CALL(*backend, hardFetchLedgerRange).Times(MAX_ATTEMPT).WillRepeatedly(Return(range));
 | 
			
		||||
 | 
			
		||||
    EXPECT_FALSE(publisher.publish(SEQ, MAX_ATTEMPT, std::chrono::milliseconds{1}));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqStopIsFalse)
 | 
			
		||||
@@ -238,16 +214,13 @@ TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqStopIsFalse)
 | 
			
		||||
    impl::LedgerPublisher publisher(ctx, backend, mockCache, mockSubscriptionManagerPtr, dummyState);
 | 
			
		||||
 | 
			
		||||
    LedgerRange const range{.minSequence = SEQ, .maxSequence = SEQ};
 | 
			
		||||
    ON_CALL(*backend, hardFetchLedgerRange(_)).WillByDefault(Return(range));
 | 
			
		||||
    EXPECT_CALL(*backend, hardFetchLedgerRange).Times(1);
 | 
			
		||||
    EXPECT_CALL(*backend, hardFetchLedgerRange).WillOnce(Return(range));
 | 
			
		||||
 | 
			
		||||
    auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, AGE);
 | 
			
		||||
    ON_CALL(*backend, fetchLedgerBySequence(SEQ, _)).WillByDefault(Return(dummyLedgerInfo));
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerBySequence(SEQ, _)).WillOnce(Return(dummyLedgerInfo));
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*backend, fetchLedgerDiff(SEQ, _)).WillByDefault(Return(std::vector<LedgerObject>{}));
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(SEQ, _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(mockCache, updateImp).Times(1);
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(SEQ, _)).WillOnce(Return(std::vector<LedgerObject>{}));
 | 
			
		||||
    EXPECT_CALL(mockCache, updateImp);
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(publisher.publish(SEQ, {}));
 | 
			
		||||
    ctx.run();
 | 
			
		||||
@@ -264,15 +237,10 @@ TEST_F(ETLLedgerPublisherTest, PublishMultipleTxInOrder)
 | 
			
		||||
 | 
			
		||||
    publisher.publish(dummyLedgerInfo);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend, fetchLedgerDiff(_, _)).Times(0);
 | 
			
		||||
 | 
			
		||||
    // mock fetch fee
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
 | 
			
		||||
    ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
    EXPECT_CALL(*backend, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
 | 
			
		||||
        .WillOnce(Return(CreateLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger).Times(1);
 | 
			
		||||
    // t1 index > t2 index
 | 
			
		||||
    TransactionAndMetadata t1;
 | 
			
		||||
    t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
 | 
			
		||||
@@ -284,22 +252,21 @@ TEST_F(ETLLedgerPublisherTest, PublishMultipleTxInOrder)
 | 
			
		||||
    t2.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30, 1).getSerializer().peekData();
 | 
			
		||||
    t2.ledgerSequence = SEQ;
 | 
			
		||||
    t2.date = 2;
 | 
			
		||||
    ON_CALL(*backend, fetchAllTransactionsInLedger(SEQ, _))
 | 
			
		||||
        .WillByDefault(Return(std::vector<TransactionAndMetadata>{t1, t2}));
 | 
			
		||||
 | 
			
		||||
    // mock fetch transactions
 | 
			
		||||
    EXPECT_CALL(*backend, fetchAllTransactionsInLedger(SEQ, _))
 | 
			
		||||
        .WillOnce(Return(std::vector<TransactionAndMetadata>{t1, t2}));
 | 
			
		||||
 | 
			
		||||
    // setLastPublishedSequence not in strand, should verify before run
 | 
			
		||||
    EXPECT_TRUE(publisher.getLastPublishedSequence());
 | 
			
		||||
    EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr =
 | 
			
		||||
        dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 2)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 2));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubBookChanges);
 | 
			
		||||
    // should call pubTransaction t2 first (greater tx index)
 | 
			
		||||
    Sequence const s;
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction(t2, _)).InSequence(s);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction(t1, _)).InSequence(s);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubTransaction(t2, _)).InSequence(s);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, pubTransaction(t1, _)).InSequence(s);
 | 
			
		||||
 | 
			
		||||
    ctx.run();
 | 
			
		||||
    // last publish time should be set
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										568
									
								
								tests/unit/etl/LoadBalancerTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										568
									
								
								tests/unit/etl/LoadBalancerTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,568 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/LoadBalancer.hpp"
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "util/Fixtures.hpp"
 | 
			
		||||
#include "util/MockNetworkValidatedLedgers.hpp"
 | 
			
		||||
#include "util/MockPrometheus.hpp"
 | 
			
		||||
#include "util/MockSource.hpp"
 | 
			
		||||
#include "util/MockSubscriptionManager.hpp"
 | 
			
		||||
#include "util/Random.hpp"
 | 
			
		||||
#include "util/config/Config.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <grpcpp/support/status.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <stdexcept>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace etl;
 | 
			
		||||
using testing::Return;
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerConstructorTests : util::prometheus::WithPrometheus, MockBackendTestStrict {
 | 
			
		||||
    StrictMockSubscriptionManagerSharedPtr subscriptionManager_;
 | 
			
		||||
    StrictMockNetworkValidatedLedgersPtr networkManager_;
 | 
			
		||||
    StrictMockSourceFactory sourceFactory_{2};
 | 
			
		||||
    boost::asio::io_context ioContext_;
 | 
			
		||||
    boost::json::value configJson_{{"etl_sources", {"source1", "source2"}}};
 | 
			
		||||
 | 
			
		||||
    std::unique_ptr<LoadBalancer>
 | 
			
		||||
    makeLoadBalancer()
 | 
			
		||||
    {
 | 
			
		||||
        return std::make_unique<LoadBalancer>(
 | 
			
		||||
            util::Config{configJson_},
 | 
			
		||||
            ioContext_,
 | 
			
		||||
            backend,
 | 
			
		||||
            subscriptionManager_,
 | 
			
		||||
            networkManager_,
 | 
			
		||||
            [this](auto&&... args) -> SourcePtr {
 | 
			
		||||
                return sourceFactory_.makeSourceMock(std::forward<decltype(args)>(args)...);
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, construct)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
    makeLoadBalancer();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_Source0Fails)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), toString);
 | 
			
		||||
    EXPECT_THROW({ makeLoadBalancer(); }, std::logic_error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_Source0ReturnsError)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled)
 | 
			
		||||
        .WillOnce(Return(boost::json::object{{"error", "some error"}}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), toString);
 | 
			
		||||
    EXPECT_THROW({ makeLoadBalancer(); }, std::logic_error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_Source1Fails)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), toString);
 | 
			
		||||
    EXPECT_THROW({ makeLoadBalancer(); }, std::logic_error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_DifferentNetworkID)
 | 
			
		||||
{
 | 
			
		||||
    auto const source1Json = boost::json::parse(R"({"result": {"info": {"network_id": 0}}})");
 | 
			
		||||
    auto const source2Json = boost::json::parse(R"({"result": {"info": {"network_id": 1}}})");
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(source1Json.as_object()));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(source2Json.as_object()));
 | 
			
		||||
    EXPECT_THROW({ makeLoadBalancer(); }, std::logic_error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_Source1FailsButAllowNoEtlIsTrue)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), toString);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
 | 
			
		||||
    configJson_.as_object()["allow_no_etl"] = true;
 | 
			
		||||
    makeLoadBalancer();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorTests, fetchETLState_DifferentNetworkIDButAllowNoEtlIsTrue)
 | 
			
		||||
{
 | 
			
		||||
    auto const source1Json = boost::json::parse(R"({"result": {"info": {"network_id": 0}}})");
 | 
			
		||||
    auto const source2Json = boost::json::parse(R"({"result": {"info": {"network_id": 1}}})");
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(source1Json.as_object()));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(source2Json.as_object()));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
 | 
			
		||||
    configJson_.as_object()["allow_no_etl"] = true;
 | 
			
		||||
    makeLoadBalancer();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerConstructorDeathTest : LoadBalancerConstructorTests {};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerConstructorDeathTest, numMarkersSpecifiedInConfigIsInvalid)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t const numMarkers = 257;
 | 
			
		||||
    configJson_.as_object()["num_markers"] = numMarkers;
 | 
			
		||||
    EXPECT_DEATH({ makeLoadBalancer(); }, ".*");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerOnConnectHookTests : LoadBalancerConstructorTests {
 | 
			
		||||
    LoadBalancerOnConnectHookTests()
 | 
			
		||||
    {
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
        loadBalancer_ = makeLoadBalancer();
 | 
			
		||||
    }
 | 
			
		||||
    std::unique_ptr<LoadBalancer> loadBalancer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnConnectHookTests, sourcesConnect)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnConnectHookTests, sourcesConnect_Source0IsNotConnected)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();  // assuming it connects and disconnects immediately
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(true));
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
 | 
			
		||||
    // Nothing is called on another connect
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnConnectHookTests, sourcesConnect_BothSourcesAreNotConnected)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
 | 
			
		||||
    // Then source 0 got connected
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerOnDisconnectHookTests : LoadBalancerOnConnectHookTests {
 | 
			
		||||
    LoadBalancerOnDisconnectHookTests()
 | 
			
		||||
    {
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
        sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
 | 
			
		||||
        // nothing happens on source 1 connect
 | 
			
		||||
        sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnDisconnectHookTests, source0Disconnects)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(true));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onDisconnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnDisconnectHookTests, source1Disconnects)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onDisconnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnDisconnectHookTests, source0DisconnectsAndConnectsBack)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(true));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onDisconnect();
 | 
			
		||||
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnDisconnectHookTests, source1DisconnectsAndConnectsBack)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onDisconnect();
 | 
			
		||||
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerOnConnectHookTests, bothSourcesDisconnectAndConnectBack)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).Times(2).WillRepeatedly(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false)).Times(2);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).Times(2).WillRepeatedly(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false)).Times(2);
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onDisconnect();
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onDisconnect();
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancer3SourcesTests : LoadBalancerConstructorTests {
 | 
			
		||||
    LoadBalancer3SourcesTests()
 | 
			
		||||
    {
 | 
			
		||||
        sourceFactory_ = StrictMockSourceFactory{3};
 | 
			
		||||
        configJson_.as_object()["etl_sources"] = {"source1", "source2", "source3"};
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(2), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(2), run);
 | 
			
		||||
        loadBalancer_ = makeLoadBalancer();
 | 
			
		||||
    }
 | 
			
		||||
    std::unique_ptr<LoadBalancer> loadBalancer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancer3SourcesTests, forwardingUpdate)
 | 
			
		||||
{
 | 
			
		||||
    // Source 2 is connected first
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(2), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(2), setForwarding(true));
 | 
			
		||||
    sourceFactory_.callbacksAt(2).onConnect();
 | 
			
		||||
 | 
			
		||||
    // Then source 0 and 1 are getting connected, but nothing should happen
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onConnect();
 | 
			
		||||
    sourceFactory_.callbacksAt(1).onConnect();
 | 
			
		||||
 | 
			
		||||
    // Source 0 got disconnected
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), isConnected()).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), setForwarding(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), isConnected()).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), setForwarding(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(2), setForwarding(false));  // only source 1 must be forwarding
 | 
			
		||||
    sourceFactory_.callbacksAt(0).onDisconnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerLoadInitialLedgerTests : LoadBalancerOnConnectHookTests {
 | 
			
		||||
    LoadBalancerLoadInitialLedgerTests()
 | 
			
		||||
    {
 | 
			
		||||
        util::Random::setSeed(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t const sequence_ = 123;
 | 
			
		||||
    uint32_t const numMarkers_ = 16;
 | 
			
		||||
    bool const cacheOnly_ = true;
 | 
			
		||||
    std::pair<std::vector<std::string>, bool> const response_ = {{"1", "2", "3"}, true};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerLoadInitialLedgerTests, load)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(loadBalancer_->loadInitialLedger(sequence_, cacheOnly_), response_.first);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerLoadInitialLedgerTests, load_source0DoesntHaveLedger)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(loadBalancer_->loadInitialLedger(sequence_, cacheOnly_), response_.first);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerLoadInitialLedgerTests, load_bothSourcesDontHaveLedger)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).Times(2).WillRepeatedly(Return(false));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(false)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(loadBalancer_->loadInitialLedger(sequence_, cacheOnly_, std::chrono::milliseconds{1}), response_.first);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerLoadInitialLedgerTests, load_source0ReturnsStatusFalse)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(std::make_pair(std::vector<std::string>{}, false)));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(loadBalancer_->loadInitialLedger(sequence_, cacheOnly_), response_.first);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerLoadInitialLedgerCustomNumMarkersTests : LoadBalancerConstructorTests {
 | 
			
		||||
    uint32_t const numMarkers_ = 16;
 | 
			
		||||
    uint32_t const sequence_ = 123;
 | 
			
		||||
    bool const cacheOnly_ = true;
 | 
			
		||||
    std::pair<std::vector<std::string>, bool> const response_ = {{"1", "2", "3"}, true};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerLoadInitialLedgerCustomNumMarkersTests, loadInitialLedger)
 | 
			
		||||
{
 | 
			
		||||
    configJson_.as_object()["num_markers"] = numMarkers_;
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
 | 
			
		||||
    util::Random::setSeed(0);
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), loadInitialLedger(sequence_, numMarkers_, cacheOnly_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(loadBalancer->loadInitialLedger(sequence_, cacheOnly_), response_.first);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerFetchLegerTests : LoadBalancerOnConnectHookTests {
 | 
			
		||||
    LoadBalancerFetchLegerTests()
 | 
			
		||||
    {
 | 
			
		||||
        util::Random::setSeed(0);
 | 
			
		||||
        response_.second.set_validated(true);
 | 
			
		||||
    }
 | 
			
		||||
    uint32_t const sequence_ = 123;
 | 
			
		||||
    bool const getObjects_ = true;
 | 
			
		||||
    bool const getObjectNeighbors_ = false;
 | 
			
		||||
    std::pair<grpc::Status, org::xrpl::rpc::v1::GetLedgerResponse> response_ =
 | 
			
		||||
        std::make_pair(grpc::Status::OK, org::xrpl::rpc::v1::GetLedgerResponse{});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerFetchLegerTests, fetch)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(loadBalancer_->fetchLedger(sequence_, getObjects_, getObjectNeighbors_).has_value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerFetchLegerTests, fetch_Source0ReturnsBadStatus)
 | 
			
		||||
{
 | 
			
		||||
    auto source0Response = response_;
 | 
			
		||||
    source0Response.first = grpc::Status::CANCELLED;
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(source0Response));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(loadBalancer_->fetchLedger(sequence_, getObjects_, getObjectNeighbors_).has_value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerFetchLegerTests, fetch_Source0ReturnsNotValidated)
 | 
			
		||||
{
 | 
			
		||||
    auto source0Response = response_;
 | 
			
		||||
    source0Response.second.set_validated(false);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(source0Response));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(loadBalancer_->fetchLedger(sequence_, getObjects_, getObjectNeighbors_).has_value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerFetchLegerTests, fetch_bothSourcesFail)
 | 
			
		||||
{
 | 
			
		||||
    auto badResponse = response_;
 | 
			
		||||
    badResponse.second.set_validated(false);
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), hasLedger(sequence_)).Times(2).WillRepeatedly(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(badResponse))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), hasLedger(sequence_)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), fetchLedger(sequence_, getObjects_, getObjectNeighbors_))
 | 
			
		||||
        .WillOnce(Return(badResponse));
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(loadBalancer_->fetchLedger(sequence_, getObjects_, getObjectNeighbors_, std::chrono::milliseconds{1})
 | 
			
		||||
                    .has_value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerForwardToRippledTests : LoadBalancerConstructorTests, SyncAsioContextTest {
 | 
			
		||||
    LoadBalancerForwardToRippledTests()
 | 
			
		||||
    {
 | 
			
		||||
        util::Random::setSeed(0);
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(0), run);
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled).WillOnce(Return(boost::json::object{}));
 | 
			
		||||
        EXPECT_CALL(sourceFactory_.sourceAt(1), run);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boost::json::object const request_{{"request", "value"}};
 | 
			
		||||
    std::optional<std::string> const clientIP_ = "some_ip";
 | 
			
		||||
    boost::json::object const response_{{"response", "other_value"}};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerForwardToRippledTests, forward)
 | 
			
		||||
{
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled(request_, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request_, clientIP_, yield), response_);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerForwardToRippledTests, source0Fails)
 | 
			
		||||
{
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled(request_, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled(request_, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request_, clientIP_, yield), response_);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerForwardToRippledTests, bothSourcesFail)
 | 
			
		||||
{
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled(request_, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(std::nullopt));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled(request_, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(std::nullopt));
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request_, clientIP_, yield), std::nullopt);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerForwardToRippledTests, forwardingCacheEnabled)
 | 
			
		||||
{
 | 
			
		||||
    configJson_.as_object()["forwarding_cache_timeout"] = 10.;
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
 | 
			
		||||
    auto const request = boost::json::object{{"command", "server_info"}};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled(request, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request, clientIP_, yield), response_);
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request, clientIP_, yield), response_);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerForwardToRippledTests, onLedgerClosedHookInvalidatesCache)
 | 
			
		||||
{
 | 
			
		||||
    configJson_.as_object()["forwarding_cache_timeout"] = 10.;
 | 
			
		||||
    auto loadBalancer = makeLoadBalancer();
 | 
			
		||||
 | 
			
		||||
    auto const request = boost::json::object{{"command", "server_info"}};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), forwardToRippled(request, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(response_));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), forwardToRippled(request, clientIP_, testing::_))
 | 
			
		||||
        .WillOnce(Return(boost::json::object{}));
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request, clientIP_, yield), response_);
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request, clientIP_, yield), response_);
 | 
			
		||||
        sourceFactory_.callbacksAt(0).onLedgerClosed();
 | 
			
		||||
        EXPECT_EQ(loadBalancer->forwardToRippled(request, clientIP_, yield), boost::json::object{});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct LoadBalancerToJsonTests : LoadBalancerOnConnectHookTests {};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoadBalancerToJsonTests, toJson)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(0), toJson).WillOnce(Return(boost::json::object{{"source1", "value1"}}));
 | 
			
		||||
    EXPECT_CALL(sourceFactory_.sourceAt(1), toJson).WillOnce(Return(boost::json::object{{"source2", "value2"}}));
 | 
			
		||||
 | 
			
		||||
    auto const expectedJson =
 | 
			
		||||
        boost::json::array({boost::json::object{{"source1", "value1"}}, boost::json::object{{"source2", "value2"}}});
 | 
			
		||||
    EXPECT_EQ(loadBalancer_->toJson(), expectedJson);
 | 
			
		||||
}
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "etl/Source.hpp"
 | 
			
		||||
#include "etl/impl/SourceImpl.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace etl;
 | 
			
		||||
using namespace etl::impl;
 | 
			
		||||
 | 
			
		||||
using testing::Return;
 | 
			
		||||
using testing::StrictMock;
 | 
			
		||||
@@ -72,7 +72,7 @@ struct ForwardingSourceMock {
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct SourceTest : public ::testing::Test {
 | 
			
		||||
struct SourceImplTest : public ::testing::Test {
 | 
			
		||||
    boost::asio::io_context ioc_;
 | 
			
		||||
 | 
			
		||||
    StrictMock<GrpcSourceMock> grpcSourceMock_;
 | 
			
		||||
@@ -94,25 +94,25 @@ struct SourceTest : public ::testing::Test {
 | 
			
		||||
        };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, run)
 | 
			
		||||
TEST_F(SourceImplTest, run)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, run());
 | 
			
		||||
    source_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, isConnected)
 | 
			
		||||
TEST_F(SourceImplTest, isConnected)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, isConnected()).WillOnce(testing::Return(true));
 | 
			
		||||
    EXPECT_TRUE(source_.isConnected());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, setForwarding)
 | 
			
		||||
TEST_F(SourceImplTest, setForwarding)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, setForwarding(true));
 | 
			
		||||
    source_.setForwarding(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, toJson)
 | 
			
		||||
TEST_F(SourceImplTest, toJson)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, validatedRange()).WillOnce(Return(std::string("some_validated_range")));
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, isConnected()).WillOnce(Return(true));
 | 
			
		||||
@@ -130,7 +130,7 @@ TEST_F(SourceTest, toJson)
 | 
			
		||||
    EXPECT_GE(std::stoi(lastMessageAgeStr), 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, toString)
 | 
			
		||||
TEST_F(SourceImplTest, toString)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, validatedRange()).WillOnce(Return(std::string("some_validated_range")));
 | 
			
		||||
 | 
			
		||||
@@ -141,14 +141,14 @@ TEST_F(SourceTest, toString)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, hasLedger)
 | 
			
		||||
TEST_F(SourceImplTest, hasLedger)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t const ledgerSeq = 123;
 | 
			
		||||
    EXPECT_CALL(*subscriptionSourceMock_, hasLedger(ledgerSeq)).WillOnce(Return(true));
 | 
			
		||||
    EXPECT_TRUE(source_.hasLedger(ledgerSeq));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, fetchLedger)
 | 
			
		||||
TEST_F(SourceImplTest, fetchLedger)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t const ledgerSeq = 123;
 | 
			
		||||
 | 
			
		||||
@@ -158,7 +158,7 @@ TEST_F(SourceTest, fetchLedger)
 | 
			
		||||
    EXPECT_EQ(actualStatus.error_code(), grpc::StatusCode::OK);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, loadInitialLedger)
 | 
			
		||||
TEST_F(SourceImplTest, loadInitialLedger)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t const ledgerSeq = 123;
 | 
			
		||||
    uint32_t const numMarkers = 3;
 | 
			
		||||
@@ -171,7 +171,7 @@ TEST_F(SourceTest, loadInitialLedger)
 | 
			
		||||
    EXPECT_TRUE(actualSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SourceTest, forwardToRippled)
 | 
			
		||||
TEST_F(SourceImplTest, forwardToRippled)
 | 
			
		||||
{
 | 
			
		||||
    boost::json::object const request = {{"some_key", "some_value"}};
 | 
			
		||||
    std::optional<std::string> const clientIp = "some_client_ip";
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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/impl/SubscriptionSourceDependencies.hpp"
 | 
			
		||||
#include "util/MockNetworkValidatedLedgers.hpp"
 | 
			
		||||
#include "util/MockSubscriptionManager.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
using namespace etl::impl;
 | 
			
		||||
using testing::StrictMock;
 | 
			
		||||
 | 
			
		||||
struct SubscriptionSourceDependenciesTest : testing::Test {
 | 
			
		||||
    std::shared_ptr<StrictMock<MockNetworkValidatedLedgers>> networkValidatedLedgers_ =
 | 
			
		||||
        std::make_shared<StrictMock<MockNetworkValidatedLedgers>>();
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<StrictMock<MockSubscriptionManager>> subscriptionManager_ =
 | 
			
		||||
        std::make_shared<StrictMock<MockSubscriptionManager>>();
 | 
			
		||||
 | 
			
		||||
    SubscriptionSourceDependencies dependencies_{networkValidatedLedgers_, subscriptionManager_};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceDependenciesTest, ForwardProposedTransaction)
 | 
			
		||||
{
 | 
			
		||||
    boost::json::object const txJson = {{"tx", "json"}};
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardProposedTransaction(txJson));
 | 
			
		||||
    dependencies_.forwardProposedTransaction(txJson);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceDependenciesTest, ForwardValidation)
 | 
			
		||||
{
 | 
			
		||||
    boost::json::object const validationJson = {{"validation", "json"}};
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardValidation(validationJson));
 | 
			
		||||
    dependencies_.forwardValidation(validationJson);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceDependenciesTest, ForwardManifest)
 | 
			
		||||
{
 | 
			
		||||
    boost::json::object const manifestJson = {{"manifest", "json"}};
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardManifest(manifestJson));
 | 
			
		||||
    dependencies_.forwardManifest(manifestJson);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceDependenciesTest, PushValidatedLedger)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t const idx = 42;
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, push(idx));
 | 
			
		||||
    dependencies_.pushValidatedLedger(idx);
 | 
			
		||||
}
 | 
			
		||||
@@ -43,26 +43,21 @@ using testing::StrictMock;
 | 
			
		||||
struct SubscriptionSourceConnectionTests : public NoLoggerFixture {
 | 
			
		||||
    SubscriptionSourceConnectionTests()
 | 
			
		||||
    {
 | 
			
		||||
        subscriptionSource_->run();
 | 
			
		||||
        subscriptionSource_.run();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boost::asio::io_context ioContext_;
 | 
			
		||||
 | 
			
		||||
    TestWsServer wsServer_{ioContext_, "0.0.0.0", 11113};
 | 
			
		||||
 | 
			
		||||
    template <typename T>
 | 
			
		||||
    using StrictMockPtr = std::shared_ptr<StrictMock<T>>;
 | 
			
		||||
 | 
			
		||||
    StrictMockPtr<MockNetworkValidatedLedgers> networkValidatedLedgers_ =
 | 
			
		||||
        std::make_shared<StrictMock<MockNetworkValidatedLedgers>>();
 | 
			
		||||
    StrictMockPtr<MockSubscriptionManager> subscriptionManager_ =
 | 
			
		||||
        std::make_shared<StrictMock<MockSubscriptionManager>>();
 | 
			
		||||
    StrictMockNetworkValidatedLedgersPtr networkValidatedLedgers_;
 | 
			
		||||
    StrictMockSubscriptionManagerSharedPtr subscriptionManager_;
 | 
			
		||||
 | 
			
		||||
    StrictMock<MockFunction<void()>> onConnectHook_;
 | 
			
		||||
    StrictMock<MockFunction<void()>> onDisconnectHook_;
 | 
			
		||||
    StrictMock<MockFunction<void()>> onLedgerClosedHook_;
 | 
			
		||||
 | 
			
		||||
    std::unique_ptr<SubscriptionSource> subscriptionSource_ = std::make_unique<SubscriptionSource>(
 | 
			
		||||
    SubscriptionSource subscriptionSource_{
 | 
			
		||||
        ioContext_,
 | 
			
		||||
        "127.0.0.1",
 | 
			
		||||
        "11113",
 | 
			
		||||
@@ -73,7 +68,7 @@ struct SubscriptionSourceConnectionTests : public NoLoggerFixture {
 | 
			
		||||
        onLedgerClosedHook_.AsStdFunction(),
 | 
			
		||||
        std::chrono::milliseconds(1),
 | 
			
		||||
        std::chrono::milliseconds(1)
 | 
			
		||||
    );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    [[maybe_unused]] TestWsConnection
 | 
			
		||||
    serverConnection(boost::asio::yield_context yield)
 | 
			
		||||
@@ -99,13 +94,13 @@ struct SubscriptionSourceConnectionTests : public NoLoggerFixture {
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceConnectionTests, ConnectionFailed)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceConnectionTests, ConnectionFailed_Retry_ConnectionFailed)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +112,7 @@ TEST_F(SubscriptionSourceConnectionTests, ReadError)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -131,21 +126,23 @@ TEST_F(SubscriptionSourceConnectionTests, ReadError_Reconnect)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceConnectionTests, IsConnected)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->isConnected());
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.isConnected());
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection = serverConnection(yield);
 | 
			
		||||
        EXPECT_TRUE(subscriptionSource_->isConnected());
 | 
			
		||||
        connection.close(yield);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).WillOnce([this]() { EXPECT_TRUE(subscriptionSource_.isConnected()); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() {
 | 
			
		||||
        EXPECT_FALSE(subscriptionSource_.isConnected());
 | 
			
		||||
        subscriptionSource_.stop();
 | 
			
		||||
    });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -170,7 +167,7 @@ TEST_F(SubscriptionSourceReadTests, GotWrongMessage_Reconnect)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +179,7 @@ TEST_F(SubscriptionSourceReadTests, GotResult)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -194,7 +191,7 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndex)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, push(123));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
@@ -209,7 +206,7 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAsString_Reconnect)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -223,21 +220,21 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersAsNumber_Reconn
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgers)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(123));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(124));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(455));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(456));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(457));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(32));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(31));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(789));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(790));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(123));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(124));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(455));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(456));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(457));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(32));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(31));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(789));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(790));
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":"123-456,789,32"}})", yield);
 | 
			
		||||
@@ -245,20 +242,20 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgers)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(123));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(124));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(455));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(456));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(457));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(32));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(31));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(789));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(790));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(123));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(124));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(455));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(456));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(457));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(32));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(31));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(789));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(790));
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_->validatedRange(), "123-456,789,32");
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_.validatedRange(), "123-456,789,32");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersWrongValue_Reconnect)
 | 
			
		||||
@@ -271,17 +268,17 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersWrongValue_Reco
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAndValidatedLedgers)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(4));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(4));
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection = connectAndSendMessage(R"({"result":{"ledger_index":123,"validated_ledgers":"1-3"}})", yield);
 | 
			
		||||
@@ -289,16 +286,16 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAndValidatedLedgers)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, push(123));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_->validatedRange(), "1-3");
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(4));
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_.validatedRange(), "1-3");
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(4));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosed)
 | 
			
		||||
@@ -309,13 +306,13 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosed)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedForwardingIsSet)
 | 
			
		||||
{
 | 
			
		||||
    subscriptionSource_->setForwarding(true);
 | 
			
		||||
    subscriptionSource_.setForwarding(true);
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection = connectAndSendMessage(R"({"type": "ledgerClosed"})", yield);
 | 
			
		||||
@@ -324,7 +321,10 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedForwardingIsSet)
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onLedgerClosedHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() {
 | 
			
		||||
        EXPECT_FALSE(subscriptionSource_.isForwarding());
 | 
			
		||||
        subscriptionSource_.stop();
 | 
			
		||||
    });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -336,7 +336,7 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndex)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, push(123));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
@@ -351,7 +351,7 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAsString_Recon
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -365,16 +365,16 @@ TEST_F(SubscriptionSourceReadTests, GorLedgerClosedWithValidatedLedgersAsNumber_
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call()).Times(2);
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithValidatedLedgers)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(0));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(0));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","validated_ledgers":"1-2"})", yield);
 | 
			
		||||
@@ -382,22 +382,22 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithValidatedLedgers)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_->validatedRange(), "1-2");
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_.validatedRange(), "1-2");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAndValidatedLedgers)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(0));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(0));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto connection =
 | 
			
		||||
@@ -406,15 +406,15 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAndValidatedLe
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*networkValidatedLedgers_, push(123));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_->hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_->hasLedger(3));
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_->validatedRange(), "1-2");
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(0));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(1));
 | 
			
		||||
    EXPECT_TRUE(subscriptionSource_.hasLedger(2));
 | 
			
		||||
    EXPECT_FALSE(subscriptionSource_.hasLedger(3));
 | 
			
		||||
    EXPECT_EQ(subscriptionSource_.validatedRange(), "1-2");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingFalse)
 | 
			
		||||
@@ -425,13 +425,13 @@ TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingFalse)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingTrue)
 | 
			
		||||
{
 | 
			
		||||
    subscriptionSource_->setForwarding(true);
 | 
			
		||||
    subscriptionSource_.setForwarding(true);
 | 
			
		||||
    boost::json::object const message = {{"transaction", "some_transaction_data"}};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
 | 
			
		||||
@@ -440,7 +440,7 @@ TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingTrue)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardProposedTransaction(message));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
@@ -453,13 +453,13 @@ TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingFalse)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingTrue)
 | 
			
		||||
{
 | 
			
		||||
    subscriptionSource_->setForwarding(true);
 | 
			
		||||
    subscriptionSource_.setForwarding(true);
 | 
			
		||||
    boost::json::object const message = {{"type", "validationReceived"}};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
 | 
			
		||||
@@ -468,7 +468,7 @@ TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingTrue)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardValidation(message));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
@@ -481,13 +481,13 @@ TEST_F(SubscriptionSourceReadTests, GotManiefstReceivedIsForwardingFalse)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionSourceReadTests, GotManifestReceivedIsForwardingTrue)
 | 
			
		||||
{
 | 
			
		||||
    subscriptionSource_->setForwarding(true);
 | 
			
		||||
    subscriptionSource_.setForwarding(true);
 | 
			
		||||
    boost::json::object const message = {{"type", "manifestReceived"}};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
 | 
			
		||||
@@ -496,7 +496,7 @@ TEST_F(SubscriptionSourceReadTests, GotManifestReceivedIsForwardingTrue)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    EXPECT_CALL(*subscriptionManager_, forwardManifest(message));
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
}
 | 
			
		||||
@@ -509,10 +509,10 @@ TEST_F(SubscriptionSourceReadTests, LastMessageTime)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectHook_, Call());
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); });
 | 
			
		||||
    EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_.stop(); });
 | 
			
		||||
    ioContext_.run();
 | 
			
		||||
 | 
			
		||||
    auto const actualLastTimeMessage = subscriptionSource_->lastMessageTime();
 | 
			
		||||
    auto const actualLastTimeMessage = subscriptionSource_.lastMessageTime();
 | 
			
		||||
    auto const now = std::chrono::steady_clock::now();
 | 
			
		||||
    auto const diff = std::chrono::duration_cast<std::chrono::milliseconds>(now - actualLastTimeMessage);
 | 
			
		||||
    EXPECT_LT(diff, std::chrono::milliseconds(100));
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,6 @@ struct ETLTransformerTest : util::prometheus::WithPrometheus, MockBackendTest {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
        state_.isStopping = false;
 | 
			
		||||
        state_.writeConflict = false;
 | 
			
		||||
        state_.isReadOnly = false;
 | 
			
		||||
@@ -78,7 +77,6 @@ struct ETLTransformerTest : util::prometheus::WithPrometheus, MockBackendTest {
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        transformer_.reset();
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,6 @@ protected:
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
        SyncAsioContextTest::SetUp();
 | 
			
		||||
        SubscriptionManagerPtr = std::make_shared<SubscriptionManager>(ctx, backend);
 | 
			
		||||
        session = std::make_shared<MockSession>();
 | 
			
		||||
@@ -78,7 +77,6 @@ protected:
 | 
			
		||||
        session.reset();
 | 
			
		||||
        SubscriptionManagerPtr.reset();
 | 
			
		||||
        SyncAsioContextTest::TearDown();
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,13 +62,11 @@ class RPCHelpersTest : public util::prometheus::WithPrometheus, public MockBacke
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
        SyncAsioContextTest::SetUp();
 | 
			
		||||
    }
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
        SyncAsioContextTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -43,23 +43,19 @@ using namespace rpc;
 | 
			
		||||
namespace json = boost::json;
 | 
			
		||||
using namespace testing;
 | 
			
		||||
 | 
			
		||||
using TestServerInfoHandler =
 | 
			
		||||
    BaseServerInfoHandler<MockSubscriptionManager, MockLoadBalancer, MockETLService, MockCounters>;
 | 
			
		||||
using TestServerInfoHandler = BaseServerInfoHandler<MockLoadBalancer, MockETLService, MockCounters>;
 | 
			
		||||
 | 
			
		||||
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
 | 
			
		||||
constexpr static auto CLIENTIP = "1.1.1.1";
 | 
			
		||||
 | 
			
		||||
class RPCServerInfoHandlerTest : public HandlerBaseTest,
 | 
			
		||||
                                 public MockLoadBalancerTest,
 | 
			
		||||
                                 public MockSubscriptionManagerTest,
 | 
			
		||||
                                 public MockCountersTest {
 | 
			
		||||
protected:
 | 
			
		||||
struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest {
 | 
			
		||||
    StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        HandlerBaseTest::SetUp();
 | 
			
		||||
        MockLoadBalancerTest::SetUp();
 | 
			
		||||
        MockSubscriptionManagerTest::SetUp();
 | 
			
		||||
        MockCountersTest::SetUp();
 | 
			
		||||
 | 
			
		||||
        backend->setRange(10, 30);
 | 
			
		||||
@@ -69,7 +65,6 @@ protected:
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockCountersTest::TearDown();
 | 
			
		||||
        MockSubscriptionManagerTest::TearDown();
 | 
			
		||||
        MockLoadBalancerTest::TearDown();
 | 
			
		||||
        HandlerBaseTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
@@ -341,7 +336,6 @@ TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
 | 
			
		||||
{
 | 
			
		||||
    MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr.get();
 | 
			
		||||
    MockCounters* rawCountersPtr = mockCountersPtr.get();
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    MockETLService* rawETLServicePtr = mockETLServicePtr.get();
 | 
			
		||||
 | 
			
		||||
    auto const empty = json::object{};
 | 
			
		||||
@@ -360,7 +354,7 @@ TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
 | 
			
		||||
    // admin calls
 | 
			
		||||
    EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
@@ -381,7 +375,6 @@ TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
 | 
			
		||||
{
 | 
			
		||||
    MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr.get();
 | 
			
		||||
    MockCounters* rawCountersPtr = mockCountersPtr.get();
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    MockETLService* rawETLServicePtr = mockETLServicePtr.get();
 | 
			
		||||
 | 
			
		||||
    auto const empty = json::object{};
 | 
			
		||||
@@ -400,7 +393,7 @@ TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
 | 
			
		||||
    // admin calls
 | 
			
		||||
    EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
@@ -427,7 +420,6 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
 | 
			
		||||
{
 | 
			
		||||
    MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr.get();
 | 
			
		||||
    MockCounters* rawCountersPtr = mockCountersPtr.get();
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    MockETLService* rawETLServicePtr = mockETLServicePtr.get();
 | 
			
		||||
 | 
			
		||||
    auto const empty = json::object{};
 | 
			
		||||
@@ -456,7 +448,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
 | 
			
		||||
    // admin calls
 | 
			
		||||
    EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
@@ -478,7 +470,6 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown)
 | 
			
		||||
{
 | 
			
		||||
    MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr.get();
 | 
			
		||||
    MockCounters* rawCountersPtr = mockCountersPtr.get();
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    MockETLService* rawETLServicePtr = mockETLServicePtr.get();
 | 
			
		||||
 | 
			
		||||
    auto const empty = json::object{};
 | 
			
		||||
@@ -502,7 +493,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown)
 | 
			
		||||
    // admin calls
 | 
			
		||||
    EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, report).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,29 +44,25 @@ namespace json = boost::json;
 | 
			
		||||
using namespace testing;
 | 
			
		||||
using namespace feed;
 | 
			
		||||
 | 
			
		||||
using TestUnsubscribeHandler = BaseUnsubscribeHandler<MockSubscriptionManager>;
 | 
			
		||||
 | 
			
		||||
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
 | 
			
		||||
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
 | 
			
		||||
 | 
			
		||||
class RPCUnsubscribeTest : public HandlerBaseTest, public MockSubscriptionManagerTest {
 | 
			
		||||
protected:
 | 
			
		||||
struct RPCUnsubscribeTest : HandlerBaseTest {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        HandlerBaseTest::SetUp();
 | 
			
		||||
        MockSubscriptionManagerTest::SetUp();
 | 
			
		||||
        session_ = std::make_shared<MockSession>();
 | 
			
		||||
    }
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockSubscriptionManagerTest::TearDown();
 | 
			
		||||
        HandlerBaseTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<feed::SubscriptionManager> subManager_;
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> session_;
 | 
			
		||||
    StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct UnsubscribeParamTestCaseBundle {
 | 
			
		||||
@@ -530,7 +526,7 @@ TEST_P(UnsubscribeParameterTest, InvalidParams)
 | 
			
		||||
{
 | 
			
		||||
    auto const testBundle = GetParam();
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const req = json::parse(testBundle.testJson);
 | 
			
		||||
        auto const output = handler.process(req, Context{yield});
 | 
			
		||||
        ASSERT_FALSE(output);
 | 
			
		||||
@@ -543,7 +539,7 @@ TEST_P(UnsubscribeParameterTest, InvalidParams)
 | 
			
		||||
TEST_F(RPCUnsubscribeTest, EmptyResponse)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(json::parse(R"({})"), Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
@@ -558,16 +554,15 @@ TEST_F(RPCUnsubscribeTest, Streams)
 | 
			
		||||
        })"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubLedger).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubTransactions).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubValidation).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubManifest).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBookChanges).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubProposedTransactions).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubLedger).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubTransactions).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubValidation).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubManifest).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubBookChanges).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubProposedTransactions).Times(1);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
@@ -584,12 +579,11 @@ TEST_F(RPCUnsubscribeTest, Accounts)
 | 
			
		||||
        ACCOUNT2
 | 
			
		||||
    ));
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubAccount(rpc::accountFromStringStrict(ACCOUNT).value(), _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubAccount(rpc::accountFromStringStrict(ACCOUNT2).value(), _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubAccount(rpc::accountFromStringStrict(ACCOUNT).value(), _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubAccount(rpc::accountFromStringStrict(ACCOUNT2).value(), _)).Times(1);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
@@ -606,14 +600,13 @@ TEST_F(RPCUnsubscribeTest, AccountsProposed)
 | 
			
		||||
        ACCOUNT2
 | 
			
		||||
    ));
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubProposedAccount(rpc::accountFromStringStrict(ACCOUNT).value(), _))
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubProposedAccount(rpc::accountFromStringStrict(ACCOUNT).value(), _))
 | 
			
		||||
        .Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubProposedAccount(rpc::accountFromStringStrict(ACCOUNT2).value(), _))
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubProposedAccount(rpc::accountFromStringStrict(ACCOUNT2).value(), _))
 | 
			
		||||
        .Times(1);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
@@ -643,12 +636,11 @@ TEST_F(RPCUnsubscribeTest, Books)
 | 
			
		||||
    auto const parsedBookMaybe = rpc::parseBook(input.as_object().at("books").as_array()[0].as_object());
 | 
			
		||||
    auto const book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(book, _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(ripple::reversed(book), _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubBook(book, _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubBook(ripple::reversed(book), _)).Times(1);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
@@ -677,11 +669,10 @@ TEST_F(RPCUnsubscribeTest, SingleBooks)
 | 
			
		||||
    auto const parsedBookMaybe = rpc::parseBook(input.as_object().at("books").as_array()[0].as_object());
 | 
			
		||||
    auto const book = std::get<ripple::Book>(parsedBookMaybe);
 | 
			
		||||
 | 
			
		||||
    MockSubscriptionManager* rawSubscriptionManagerPtr = mockSubscriptionManagerPtr.get();
 | 
			
		||||
    EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(book, _)).Times(1);
 | 
			
		||||
    EXPECT_CALL(*mockSubscriptionManagerPtr, unsubBook(book, _)).Times(1);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{TestUnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const handler = AnyHandler{UnsubscribeHandler{backend, mockSubscriptionManagerPtr}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield, session_});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_TRUE(output.result->as_object().empty());
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								tests/unit/util/RandomTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								tests/unit/util/RandomTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, 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 "util/Random.hpp"
 | 
			
		||||
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace util;
 | 
			
		||||
 | 
			
		||||
struct RandomTests : public ::testing::Test {
 | 
			
		||||
    static std::vector<int>
 | 
			
		||||
    generateRandoms(size_t const numRandoms = 1000)
 | 
			
		||||
    {
 | 
			
		||||
        std::vector<int> v;
 | 
			
		||||
        v.reserve(numRandoms);
 | 
			
		||||
        std::ranges::generate_n(std::back_inserter(v), numRandoms, []() { return Random::uniform(0, 1000); });
 | 
			
		||||
        return v;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(RandomTests, Uniform)
 | 
			
		||||
{
 | 
			
		||||
    std::ranges::for_each(generateRandoms(), [](int const& e) {
 | 
			
		||||
        EXPECT_GE(e, 0);
 | 
			
		||||
        EXPECT_LE(e, 1000);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RandomTests, FixedSeed)
 | 
			
		||||
{
 | 
			
		||||
    Random::setSeed(42);
 | 
			
		||||
    std::vector<int> const v1 = generateRandoms();
 | 
			
		||||
 | 
			
		||||
    Random::setSeed(42);
 | 
			
		||||
    std::vector<int> const v2 = generateRandoms();
 | 
			
		||||
 | 
			
		||||
    ASSERT_EQ(v1.size(), v2.size());
 | 
			
		||||
    for (size_t i = 0; i < v1.size(); ++i) {
 | 
			
		||||
        EXPECT_EQ(v1[i], v2[i]);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -66,15 +66,10 @@ struct MockWsBase : public web::ConnectionBase {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class WebRPCServerHandlerTest : public util::prometheus::WithPrometheus,
 | 
			
		||||
                                public MockBackendTest,
 | 
			
		||||
                                public SyncAsioContextTest {
 | 
			
		||||
protected:
 | 
			
		||||
struct WebRPCServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTest, SyncAsioContextTest {
 | 
			
		||||
    void
 | 
			
		||||
    SetUp() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::SetUp();
 | 
			
		||||
 | 
			
		||||
        etl = std::make_shared<MockETLService>();
 | 
			
		||||
        rpcEngine = std::make_shared<MockAsyncRPCEngine>();
 | 
			
		||||
        tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
 | 
			
		||||
@@ -82,12 +77,6 @@ protected:
 | 
			
		||||
        handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    TearDown() override
 | 
			
		||||
    {
 | 
			
		||||
        MockBackendTest::TearDown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
 | 
			
		||||
    std::shared_ptr<MockETLService> etl;
 | 
			
		||||
    std::shared_ptr<util::TagDecoratorFactory> tagFactory;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user