Cover LoadBalancer with tests (#1394)

Fixes #680. Fixes #1222.
This commit is contained in:
Sergey Kuznetsov
2024-05-15 14:02:36 +01:00
committed by GitHub
parent f74b89cc8d
commit da10535bc0
61 changed files with 2769 additions and 1466 deletions

View File

@@ -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

View File

@@ -21,7 +21,7 @@ jobs:
- name: Run formatters
id: run_formatters
run: |
./.githooks/check-format
./.githooks/check-format --diff
shell: bash
check_docs:

View File

@@ -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"

View File

@@ -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");

View File

@@ -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(); });
}
}

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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";

View File

@@ -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
View 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

View File

@@ -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) {

View File

@@ -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
*

View File

@@ -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

View File

@@ -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

View File

@@ -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;
};
/**

View 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

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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_;
};

View File

@@ -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();
}
};

View File

@@ -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();
}
};

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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;
};

View File

@@ -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());
}

View File

@@ -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_};
}

View File

@@ -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

View 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);
}

View File

@@ -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";

View File

@@ -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);
}

View File

@@ -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));

View File

@@ -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();
}
};

View File

@@ -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();
}
};

View File

@@ -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();
}
};

View File

@@ -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));

View File

@@ -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());

View 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]);
};
}

View File

@@ -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;