diff --git a/CMakeLists.txt b/CMakeLists.txt index 521e9213..11fe3ed9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,13 +97,14 @@ target_sources (clio PRIVATE src/data/cassandra/Handle.cpp src/data/cassandra/SettingsProvider.cpp ## ETL - src/etl/Source.cpp - src/etl/ProbingSource.cpp src/etl/NFTHelpers.cpp src/etl/ETLService.cpp src/etl/ETLState.cpp src/etl/LoadBalancer.cpp - src/etl/impl/ForwardCache.cpp + src/etl/Source.cpp + src/etl/impl/ForwardingSource.cpp + src/etl/impl/GrpcSource.cpp + src/etl/impl/SubscriptionSource.cpp ## Feed src/feed/SubscriptionManager.cpp src/feed/impl/TransactionFeed.cpp @@ -164,6 +165,7 @@ target_sources (clio PRIVATE src/util/prometheus/OStream.cpp src/util/prometheus/Prometheus.cpp src/util/Random.cpp + src/util/Retry.cpp src/util/requests/RequestBuilder.cpp src/util/requests/Types.cpp src/util/requests/WsConnection.cpp @@ -212,6 +214,7 @@ if (tests) unittests/util/prometheus/MetricBuilderTests.cpp unittests/util/prometheus/MetricsFamilyTests.cpp unittests/util/prometheus/OStreamTests.cpp + unittests/util/RetryTests.cpp ## Async framework unittests/util/async/AnyExecutionContextTests.cpp unittests/util/async/AnyStrandTests.cpp @@ -223,13 +226,18 @@ if (tests) unittests/util/requests/SslContextTests.cpp unittests/util/requests/WsConnectionTests.cpp # ETL + unittests/etl/AmendmentBlockHandlerTests.cpp + unittests/etl/CacheLoaderTests.cpp + unittests/etl/ETLStateTests.cpp unittests/etl/ExtractionDataPipeTests.cpp unittests/etl/ExtractorTests.cpp - unittests/etl/TransformerTests.cpp - unittests/etl/CacheLoaderTests.cpp - unittests/etl/AmendmentBlockHandlerTests.cpp + unittests/etl/ForwardingSourceTests.cpp + unittests/etl/GrpcSourceTests.cpp + unittests/etl/SourceTests.cpp + unittests/etl/SubscriptionSourceTests.cpp + unittests/etl/SubscriptionSourceDependenciesTests.cpp unittests/etl/LedgerPublisherTests.cpp - unittests/etl/ETLStateTests.cpp + unittests/etl/TransformerTests.cpp # RPC unittests/rpc/ErrorTests.cpp unittests/rpc/BaseTests.cpp diff --git a/src/data/cassandra/Concepts.hpp b/src/data/cassandra/Concepts.hpp index 47f35f1a..c532429f 100644 --- a/src/data/cassandra/Concepts.hpp +++ b/src/data/cassandra/Concepts.hpp @@ -123,9 +123,6 @@ concept SomeRetryPolicy = requires(T a, boost::asio::io_context ioc, CassandraEr { a.retry([]() {}) } -> std::same_as; - { - a.calculateDelay(attempt) - } -> std::same_as; }; } // namespace data::cassandra diff --git a/src/data/cassandra/impl/RetryPolicy.hpp b/src/data/cassandra/impl/RetryPolicy.hpp index d2b6e1cc..cc5e63c2 100644 --- a/src/data/cassandra/impl/RetryPolicy.hpp +++ b/src/data/cassandra/impl/RetryPolicy.hpp @@ -19,16 +19,16 @@ #pragma once -#include "data/cassandra/Handle.hpp" +#include "data/cassandra/Error.hpp" #include "data/cassandra/Types.hpp" -#include "util/Expected.hpp" +#include "util/Retry.hpp" #include "util/log/Logger.hpp" #include +#include +#include -#include #include -#include namespace data::cassandra::impl { @@ -38,14 +38,18 @@ namespace data::cassandra::impl { class ExponentialBackoffRetryPolicy { util::Logger log_{"Backend"}; - boost::asio::steady_timer timer_; - uint32_t attempt_ = 0u; + util::Retry retry_; public: /** * @brief Create a new retry policy instance with the io_context provided */ - ExponentialBackoffRetryPolicy(boost::asio::io_context& ioc) : timer_{boost::asio::make_strand(ioc)} + ExponentialBackoffRetryPolicy(boost::asio::io_context& ioc) + : retry_(util::makeRetryExponentialBackoff( + std::chrono::milliseconds(1), + std::chrono::seconds(1), + boost::asio::make_strand(ioc) + )) { } @@ -57,9 +61,9 @@ public: [[nodiscard]] bool shouldRetry([[maybe_unused]] CassandraError err) { - auto const delay = calculateDelay(attempt_); - LOG(log_.error()) << "Cassandra write error: " << err << ", current retries " << attempt_ << ", retrying in " - << delay.count() << " milliseconds"; + auto const delayMs = std::chrono::duration_cast(retry_.delayValue()).count(); + LOG(log_.error()) << "Cassandra write error: " << err << ", current retries " << retry_.attemptNumber() + << ", retrying in " << delayMs << " milliseconds"; return true; // keep retrying forever } @@ -73,20 +77,7 @@ public: void retry(Fn&& fn) { - timer_.expires_after(calculateDelay(attempt_++)); - timer_.async_wait([fn = std::forward(fn)]([[maybe_unused]] auto const& err) { - // todo: deal with cancellation (thru err) - fn(); - }); - } - - /** - * @brief Calculates the wait time before attempting another retry - */ - static std::chrono::milliseconds - calculateDelay(uint32_t attempt) - { - return std::chrono::milliseconds{lround(std::pow(2, std::min(10u, attempt)))}; + retry_.retry(std::forward(fn)); } }; diff --git a/src/etl/ETLService.hpp b/src/etl/ETLService.hpp index 24e35a01..85d11ba6 100644 --- a/src/etl/ETLService.hpp +++ b/src/etl/ETLService.hpp @@ -24,7 +24,6 @@ #include "etl/ETLHelpers.hpp" #include "etl/ETLState.hpp" #include "etl/LoadBalancer.hpp" -#include "etl/Source.hpp" #include "etl/SystemState.hpp" #include "etl/impl/AmendmentBlock.hpp" #include "etl/impl/CacheLoader.hpp" diff --git a/src/etl/LoadBalancer.cpp b/src/etl/LoadBalancer.cpp index 7320eacd..d1fbb002 100644 --- a/src/etl/LoadBalancer.cpp +++ b/src/etl/LoadBalancer.cpp @@ -23,9 +23,7 @@ #include "etl/ETLHelpers.hpp" #include "etl/ETLService.hpp" #include "etl/ETLState.hpp" -#include "etl/ProbingSource.hpp" #include "etl/Source.hpp" -#include "util/Assert.hpp" #include "util/Random.hpp" #include "util/log/Logger.hpp" @@ -52,22 +50,6 @@ using namespace util; namespace etl { -std::unique_ptr -LoadBalancer::make_Source( - Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr validatedLedgers, - LoadBalancer& balancer -) -{ - auto src = std::make_unique(config, ioc, backend, subscriptions, validatedLedgers, balancer); - src->run(); - - return src; -} - std::shared_ptr LoadBalancer::make_LoadBalancer( Config const& config, @@ -98,7 +80,7 @@ LoadBalancer::LoadBalancer( auto const allowNoEtl = config.valueOr("allow_no_etl", false); auto const checkOnETLFailure = [this, allowNoEtl](std::string const& log) { - LOG(log_.error()) << log; + LOG(log_.warn()) << log; if (!allowNoEtl) { LOG(log_.error()) << "Set allow_no_etl as true in config to allow clio run without valid ETL sources."; @@ -107,15 +89,26 @@ LoadBalancer::LoadBalancer( }; for (auto const& entry : config.array("etl_sources")) { - std::unique_ptr source = make_Source(entry, ioc, backend, subscriptions, validatedLedgers, *this); + auto source = make_Source( + entry, + ioc, + backend, + subscriptions, + validatedLedgers, + [this]() { + if (not hasForwardingSource_) + chooseForwardingSource(); + }, + [this]() { chooseForwardingSource(); } + ); // 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) { checkOnETLFailure(fmt::format( @@ -128,11 +121,17 @@ 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()) checkOnETLFailure("No ETL sources configured. Please check the configuration"); + + // 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(); + } } LoadBalancer::~LoadBalancer() @@ -146,11 +145,11 @@ LoadBalancer::loadInitialLedger(uint32_t sequence, bool cacheOnly) std::vector response; auto const success = 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(); + << " Sequence = " << sequence << " source = " << source.toString(); } else { response = std::move(data); } @@ -168,17 +167,17 @@ LoadBalancer::fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObje GetLedgerResponseType response; bool const success = 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 @@ -203,7 +202,7 @@ LoadBalancer::forwardToRippled( auto numAttempts = 0u; while (numAttempts < sources_.size()) { - if (auto res = sources_[sourceIdx]->forwardToRippled(request, clientIp, yield)) + if (auto res = sources_[sourceIdx].forwardToRippled(request, clientIp, yield)) return res; sourceIdx = (sourceIdx + 1) % sources_.size(); @@ -213,27 +212,12 @@ LoadBalancer::forwardToRippled( return {}; } -bool -LoadBalancer::shouldPropagateTxnStream(Source* in) const -{ - for (auto& src : sources_) { - ASSERT(src != nullptr, "Source is nullptr"); - - // We pick the first Source encountered that is connected - if (src->isConnected()) - return *src == *in; - } - - // If no sources connected, then this stream has not been forwarded - return true; -} - boost::json::value LoadBalancer::toJson() const { boost::json::array ret; for (auto& src : sources_) - ret.push_back(src->toJson()); + ret.push_back(src.toJson()); return ret; } @@ -252,23 +236,23 @@ LoadBalancer::execute(Func f, uint32_t ledgerSequence) 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(); @@ -293,4 +277,17 @@ LoadBalancer::getETLState() noexcept return etlState_; } +void +LoadBalancer::chooseForwardingSource() +{ + hasForwardingSource_ = false; + for (auto& source : sources_) { + if (source.isConnected()) { + source.setForwarding(true); + hasForwardingSource_ = true; + return; + } + } +} + } // namespace etl diff --git a/src/etl/LoadBalancer.hpp b/src/etl/LoadBalancer.hpp index 51910407..aaadc072 100644 --- a/src/etl/LoadBalancer.hpp +++ b/src/etl/LoadBalancer.hpp @@ -22,6 +22,7 @@ #include "data/BackendInterface.hpp" #include "etl/ETLHelpers.hpp" #include "etl/ETLState.hpp" +#include "etl/Source.hpp" #include "feed/SubscriptionManager.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" @@ -36,6 +37,7 @@ #include #include +#include #include #include #include @@ -44,7 +46,6 @@ #include namespace etl { -class Source; class ProbingSource; } // namespace etl @@ -71,10 +72,11 @@ private: static constexpr std::uint32_t DEFAULT_DOWNLOAD_RANGES = 16; util::Logger log_{"ETL"}; - std::vector> sources_; + std::vector sources_; std::optional etlState_; std::uint32_t downloadRanges_ = DEFAULT_DOWNLOAD_RANGES; /*< The number of markers to use when downloading intial ledger */ + std::atomic_bool hasForwardingSource_{false}; public: /** @@ -138,19 +140,6 @@ public: OptionalGetLedgerResponseType fetchLedger(uint32_t ledgerSequence, bool getObjects, bool getObjectNeighbors); - /** - * @brief Determine whether messages received on the transactions_proposed stream should be forwarded to subscribing - * clients. - * - * The server subscribes to transactions_proposed on multiple Sources, yet only forwards messages from one source at - * any given time (to avoid sending duplicate messages to clients). - * - * @param in Source in question - * @return true if messages should be forwarded - */ - bool - shouldPropagateTxnStream(Source* in) const; - /** * @return JSON representation of the state of this load balancer. */ @@ -180,26 +169,6 @@ public: getETLState() noexcept; private: - /** - * @brief A factory function for the ETL source. - * - * @param config The configuration to use - * @param ioc The io_context to run on - * @param backend BackendInterface implementation - * @param subscriptions Subscription manager - * @param validatedLedgers The network validated ledgers datastructure - * @param balancer The load balancer - */ - static std::unique_ptr - make_Source( - util::Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr validatedLedgers, - LoadBalancer& balancer - ); - /** * @brief Execute a function on a randomly selected source. * @@ -215,5 +184,12 @@ private: template bool execute(Func f, uint32_t ledgerSequence); + + /** + * @brief Choose a new source to forward requests + */ + void + chooseForwardingSource(); }; + } // namespace etl diff --git a/src/etl/ProbingSource.cpp b/src/etl/ProbingSource.cpp deleted file mode 100644 index 8cab26f7..00000000 --- a/src/etl/ProbingSource.cpp +++ /dev/null @@ -1,232 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2022, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "etl/ProbingSource.hpp" - -#include "data/BackendInterface.hpp" -#include "etl/ETLHelpers.hpp" -#include "etl/LoadBalancer.hpp" -#include "etl/Source.hpp" -#include "feed/SubscriptionManager.hpp" -#include "util/config/Config.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace etl { - -ProbingSource::ProbingSource( - util::Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr nwvl, - LoadBalancer& balancer, - boost::asio::ssl::context sslCtx -) - : sslCtx_{std::move(sslCtx)} - , sslSrc_{make_shared< - SslSource>(config, ioc, std::ref(sslCtx_), backend, subscriptions, nwvl, balancer, make_SSLHooks())} - , plainSrc_{make_shared(config, ioc, backend, subscriptions, nwvl, balancer, make_PlainHooks())} -{ -} - -void -ProbingSource::run() -{ - sslSrc_->run(); - plainSrc_->run(); -} - -void -ProbingSource::pause() -{ - sslSrc_->pause(); - plainSrc_->pause(); -} - -void -ProbingSource::resume() -{ - sslSrc_->resume(); - plainSrc_->resume(); -} - -bool -ProbingSource::isConnected() const -{ - return currentSrc_ && currentSrc_->isConnected(); -} - -bool -ProbingSource::hasLedger(uint32_t sequence) const -{ - if (!currentSrc_) - return false; - return currentSrc_->hasLedger(sequence); -} - -boost::json::object -ProbingSource::toJson() const -{ - if (!currentSrc_) { - boost::json::object sourcesJson = { - {"ws", plainSrc_->toJson()}, - {"wss", sslSrc_->toJson()}, - }; - - return { - {"probing", sourcesJson}, - }; - } - return currentSrc_->toJson(); -} - -std::string -ProbingSource::toString() const -{ - if (!currentSrc_) - return "{probing... ws: " + plainSrc_->toString() + ", wss: " + sslSrc_->toString() + "}"; - return currentSrc_->toString(); -} - -boost::uuids::uuid -ProbingSource::token() const -{ - if (!currentSrc_) - return boost::uuids::nil_uuid(); - return currentSrc_->token(); -} - -std::pair, bool> -ProbingSource::loadInitialLedger(std::uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly) -{ - if (!currentSrc_) - return {{}, false}; - return currentSrc_->loadInitialLedger(sequence, numMarkers, cacheOnly); -} - -std::pair -ProbingSource::fetchLedger(uint32_t sequence, bool getObjects, bool getObjectNeighbors) -{ - if (!currentSrc_) - return {}; - return currentSrc_->fetchLedger(sequence, getObjects, getObjectNeighbors); -} - -std::optional -ProbingSource::forwardToRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield -) const -{ - if (!currentSrc_) // Source may connect to rippled before the connection built to check the validity - { - if (auto res = plainSrc_->forwardToRippled(request, clientIp, yield)) - return res; - - return sslSrc_->forwardToRippled(request, clientIp, yield); - } - return currentSrc_->forwardToRippled(request, clientIp, yield); -} - -std::optional -ProbingSource::requestFromRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield -) const -{ - if (!currentSrc_) - return {}; - return currentSrc_->requestFromRippled(request, clientIp, yield); -} - -SourceHooks -ProbingSource::make_SSLHooks() noexcept -{ - return {// onConnected - [this](auto ec) { - std::lock_guard const lck(mtx_); - if (currentSrc_) - return SourceHooks::Action::STOP; - - if (!ec) { - plainSrc_->pause(); - currentSrc_ = sslSrc_; - LOG(log_.info()) << "Selected WSS as the main source: " << currentSrc_->toString(); - } - return SourceHooks::Action::PROCEED; - }, - // onDisconnected - [this](auto /* ec */) { - std::lock_guard const lck(mtx_); - if (currentSrc_) { - currentSrc_ = nullptr; - plainSrc_->resume(); - } - return SourceHooks::Action::STOP; - } - }; -} - -SourceHooks -ProbingSource::make_PlainHooks() noexcept -{ - return {// onConnected - [this](auto ec) { - std::lock_guard const lck(mtx_); - if (currentSrc_) - return SourceHooks::Action::STOP; - - if (!ec) { - sslSrc_->pause(); - currentSrc_ = plainSrc_; - LOG(log_.info()) << "Selected Plain WS as the main source: " << currentSrc_->toString(); - } - return SourceHooks::Action::PROCEED; - }, - // onDisconnected - [this](auto /* ec */) { - std::lock_guard const lck(mtx_); - if (currentSrc_) { - currentSrc_ = nullptr; - sslSrc_->resume(); - } - return SourceHooks::Action::STOP; - } - }; -}; -} // namespace etl diff --git a/src/etl/ProbingSource.hpp b/src/etl/ProbingSource.hpp deleted file mode 100644 index 6466f3d0..00000000 --- a/src/etl/ProbingSource.hpp +++ /dev/null @@ -1,147 +0,0 @@ -//------------------------------------------------------------------------------ -/* - 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 "data/BackendInterface.hpp" -#include "etl/ETLHelpers.hpp" -#include "etl/LoadBalancer.hpp" -#include "etl/Source.hpp" -#include "util/config/Config.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace etl { - -/** - * @brief This Source implementation attempts to connect over both secure websocket and plain websocket. - * - * First to connect pauses the other and the probing is considered done at this point. - * If however the connected source loses connection the probing is kickstarted again. - */ -class ProbingSource : public Source { -public: - // TODO: inject when unit tests will be written for ProbingSource - using GetLedgerResponseType = org::xrpl::rpc::v1::GetLedgerResponse; - -private: - util::Logger log_{"ETL"}; - - std::mutex mtx_; - boost::asio::ssl::context sslCtx_; - std::shared_ptr sslSrc_; - std::shared_ptr plainSrc_; - std::shared_ptr currentSrc_; - -public: - /** - * @brief Create an instance of the probing source. - * - * @param config The configuration to use - * @param ioc io context to run on - * @param backend BackendInterface implementation - * @param subscriptions Subscription manager - * @param nwvl The network validated ledgers datastructure - * @param balancer Load balancer to use - * @param sslCtx The SSL context to use; defaults to tlsv12 - */ - ProbingSource( - util::Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr nwvl, - LoadBalancer& balancer, - boost::asio::ssl::context sslCtx = boost::asio::ssl::context{boost::asio::ssl::context::tlsv12} - ); - - ~ProbingSource() override = default; - - void - run() override; - - void - pause() override; - - void - resume() override; - - bool - isConnected() const override; - - bool - hasLedger(uint32_t sequence) const override; - - boost::json::object - toJson() const override; - - std::string - toString() const override; - - std::pair, bool> - loadInitialLedger(std::uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) override; - - std::pair - fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) override; - - std::optional - forwardToRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield - ) const override; - - boost::uuids::uuid - token() const override; - -private: - std::optional - requestFromRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield - ) const override; - - SourceHooks - make_SSLHooks() noexcept; - - SourceHooks - make_PlainHooks() noexcept; -}; -} // namespace etl diff --git a/src/etl/Source.cpp b/src/etl/Source.cpp index cb089bda..f87213ff 100644 --- a/src/etl/Source.cpp +++ b/src/etl/Source.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2022, the clio developers. + 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 @@ -19,162 +19,51 @@ #include "etl/Source.hpp" -#include "util/log/Logger.hpp" +#include "data/BackendInterface.hpp" +#include "etl/ETLHelpers.hpp" +#include "feed/SubscriptionManager.hpp" +#include "util/config/Config.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include #include +#include namespace etl { -static boost::beast::websocket::stream_base::timeout -make_TimeoutOption() -{ - return boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::client); -} +template class SourceImpl<>; -void -PlainSource::close(bool startAgain) -{ - timer_.cancel(); - boost::asio::post(strand_, [this, startAgain]() { - if (closing_) - return; - - if (derived().ws().is_open()) { - // onStop() also calls close(). If the async_close is called twice, - // an assertion fails. Using closing_ makes sure async_close is only - // called once - closing_ = true; - derived().ws().async_close(boost::beast::websocket::close_code::normal, [this, startAgain](auto ec) { - if (ec) { - LOG(log_.error()) << "async_close: error code = " << ec << " - " << toString(); - } - closing_ = false; - if (startAgain) { - ws_ = std::make_unique(strand_); - run(); - } - }); - } else if (startAgain) { - ws_ = std::make_unique(strand_); - run(); - } - }); -} - -void -SslSource::close(bool startAgain) -{ - timer_.cancel(); - boost::asio::post(strand_, [this, startAgain]() { - if (closing_) - return; - - if (derived().ws().is_open()) { - // onStop() also calls close(). If the async_close is called twice, an assertion fails. Using closing_ - // makes sure async_close is only called once - closing_ = true; - derived().ws().async_close(boost::beast::websocket::close_code::normal, [this, startAgain](auto ec) { - if (ec) { - LOG(log_.error()) << "async_close: error code = " << ec << " - " << toString(); - } - closing_ = false; - if (startAgain) { - ws_ = std::make_unique(strand_, *sslCtx_); - run(); - } - }); - } else if (startAgain) { - ws_ = std::make_unique(strand_, *sslCtx_); - run(); - } - }); -} - -void -PlainSource::onConnect( - boost::beast::error_code ec, - boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint +Source +make_Source( + util::Config const& config, + boost::asio::io_context& ioc, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr validatedLedgers, + Source::OnDisconnectHook onDisconnect, + Source::OnConnectHook onConnect ) { - if (ec) { - // start over - reconnect(ec); - } else { - connected_ = true; - numFailures_ = 0; + auto const ip = config.valueOr("ip", {}); + auto const wsPort = config.valueOr("ws_port", {}); + auto const grpcPort = config.valueOr("grpc_port", {}); - // Websocket stream has it's own timeout system - boost::beast::get_lowest_layer(derived().ws()).expires_never(); + impl::GrpcSource grpcSource{ip, grpcPort, std::move(backend)}; + auto subscriptionSource = std::make_unique( + ioc, + ip, + wsPort, + std::move(validatedLedgers), + std::move(subscriptions), + std::move(onConnect), + std::move(onDisconnect) + ); + impl::ForwardingSource forwardingSource{ip, wsPort}; - derived().ws().set_option(make_TimeoutOption()); - derived().ws().set_option( - boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) { - req.set(boost::beast::http::field::user_agent, "clio-client"); - req.set("X-User", "clio-client"); - }) - ); - - // Update the host_ string. This will provide the value of the - // Host HTTP header during the WebSocket handshake. - // See https://tools.ietf.org/html/rfc7230#section-5.4 - auto host = ip_ + ':' + std::to_string(endpoint.port()); - derived().ws().async_handshake(host, "/", [this](auto ec) { onHandshake(ec); }); - } + return Source{ + ip, wsPort, grpcPort, std::move(grpcSource), std::move(subscriptionSource), std::move(forwardingSource) + }; } -void -SslSource::onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint) -{ - if (ec) { - // start over - reconnect(ec); - } else { - connected_ = true; - numFailures_ = 0; - - // Websocket stream has it's own timeout system - boost::beast::get_lowest_layer(derived().ws()).expires_never(); - - derived().ws().set_option(make_TimeoutOption()); - derived().ws().set_option( - boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) { - req.set(boost::beast::http::field::user_agent, "clio-client"); - req.set("X-User", "clio-client"); - }) - ); - - // Update the host_ string. This will provide the value of the - // Host HTTP header during the WebSocket handshake. - // See https://tools.ietf.org/html/rfc7230#section-5.4 - auto host = ip_ + ':' + std::to_string(endpoint.port()); - ws().next_layer().async_handshake(boost::asio::ssl::stream_base::client, [this, endpoint](auto ec) { - onSslHandshake(ec, endpoint); - }); - } -} - -void -SslSource::onSslHandshake( - boost::beast::error_code ec, - boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint -) -{ - if (ec) { - reconnect(ec); - } else { - auto host = ip_ + ':' + std::to_string(endpoint.port()); - ws().async_handshake(host, "/", [this](auto ec) { onHandshake(ec); }); - } -} } // namespace etl diff --git a/src/etl/Source.hpp b/src/etl/Source.hpp index 2da46585..4efec3e7 100644 --- a/src/etl/Source.hpp +++ b/src/etl/Source.hpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2022, the clio developers. + 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 @@ -21,121 +21,123 @@ #include "data/BackendInterface.hpp" #include "etl/ETLHelpers.hpp" -#include "etl/LoadBalancer.hpp" -#include "etl/impl/AsyncData.hpp" -#include "etl/impl/ForwardCache.hpp" +#include "etl/impl/ForwardingSource.hpp" +#include "etl/impl/GrpcSource.hpp" +#include "etl/impl/SubscriptionSource.hpp" #include "feed/SubscriptionManager.hpp" -#include "util/Assert.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include #include -#include -#include #include -#include -#include #include -#include #include -#include #include #include #include -namespace feed { -class SubscriptionManager; -} // namespace feed - -// TODO: we use Source so that we can store a vector of Sources -// but we also use CRTP for implementation of the common logic - this is a bit strange because CRTP as used here is -// supposed to be used instead of an abstract base. -// Maybe we should rework this a bit. At this point there is not too much use in the CRTP implementation - we can move -// things into the base class instead. - namespace etl { -class ProbingSource; +template < + typename GrpcSourceType = impl::GrpcSource, + typename SubscriptionSourceTypePtr = std::unique_ptr, + typename ForwardingSourceType = impl::ForwardingSource> +class SourceImpl { + std::string ip_; + std::string wsPort_; + std::string grpcPort_; + + GrpcSourceType grpcSource_; + SubscriptionSourceTypePtr subscriptionSource_; + ForwardingSourceType forwardingSource_; -/** - * @brief Base class for all ETL sources. - * - * Note: Since sources below are implemented via CRTP, it sort of makes no sense to have a virtual base class. - * We should consider using a vector of ProbingSources instead of vector of unique ptrs to this virtual base. - */ -class Source { public: + using OnConnectHook = impl::SubscriptionSource::OnConnectHook; + using OnDisconnectHook = impl::SubscriptionSource::OnDisconnectHook; + + template + requires std::is_same_v && + std::is_same_v + 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(grpcSource)) + , subscriptionSource_(std::move(subscriptionSource)) + , forwardingSource_(std::forward(forwardingSource)) + { + } + + /** + * @brief Run subscriptions loop of the source + */ + void + run() + { + subscriptionSource_->run(); + } + /** @return true if source is connected; false otherwise */ - virtual bool - isConnected() const = 0; + bool + isConnected() const + { + return subscriptionSource_->isConnected(); + } + + /** + * @brief Set the forwarding state of the source. + * + * @param isForwarding Whether to forward or not + */ + void + setForwarding(bool isForwarding) + { + subscriptionSource_->setForwarding(isForwarding); + } /** @return JSON representation of the source */ - virtual boost::json::object - toJson() const = 0; + boost::json::object + toJson() const + { + boost::json::object res; - /** @brief Runs the source */ - virtual void - run() = 0; + res["validated_range"] = subscriptionSource_->validatedRange(); + res["is_connected"] = std::to_string(static_cast(subscriptionSource_->isConnected())); + res["ip"] = ip_; + res["ws_port"] = wsPort_; + res["grpc_port"] = grpcPort_; - /** @brief Request to pause the source (i.e. disconnect and do nothing) */ - virtual void - pause() = 0; + 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::steady_clock::now() - last).count() + ); + } - /** @brief Reconnect and resume this source */ - virtual void - resume() = 0; + return res; + } /** @return String representation of the source (for debug) */ - virtual std::string - toString() const = 0; + std::string + toString() const + { + return "{validated range: " + subscriptionSource_->validatedRange() + ", ip: " + ip_ + + ", web socket port: " + wsPort_ + ", grpc port: " + grpcPort_ + "}"; + } /** * @brief Check if ledger is known by this source. @@ -143,8 +145,11 @@ public: * @param sequence The ledger sequence to check * @return true if ledger is in the range of this source; false otherwise */ - virtual bool - hasLedger(uint32_t sequence) const = 0; + bool + hasLedger(uint32_t sequence) const + { + return subscriptionSource_->hasLedger(sequence); + } /** * @brief Fetch data for a specific ledger. @@ -157,8 +162,11 @@ public: * @param getObjectNeighbors Whether to request object neighbors; defaults to false * @return A std::pair of the response status and the response itself */ - virtual std::pair - fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) = 0; + std::pair + fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) + { + return grpcSource_.fetchLedger(sequence, getObjects, getObjectNeighbors); + } /** * @brief Download a ledger in full. @@ -166,10 +174,13 @@ public: * @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 successfull + * @return A std::pair of the data and a bool indicating whether the download was successful */ - virtual std::pair, bool> - loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) = 0; + std::pair, bool> + loadInitialLedger(uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) + { + return grpcSource_.loadInitialLedger(sequence, numMarkers, cacheOnly); + } /** * @brief Forward a request to rippled. @@ -179,820 +190,39 @@ public: * @param yield The coroutine context * @return Response wrapped in an optional on success; nullopt otherwise */ - virtual std::optional + std::optional forwardToRippled( boost::json::object const& request, - std::optional const& forwardToRippledclientIp, + std::optional const& forwardToRippledClientIp, boost::asio::yield_context yield - ) const = 0; - - /** - * @return A token that uniquely identifies this source instance. - */ - virtual boost::uuids::uuid - token() const = 0; - - virtual ~Source() = default; - - /** - * @brief Comparison is done via comparing tokens provided by the token() function. - * - * @param other The other source to compare to - * @return true if sources are equal; false otherwise - */ - bool - operator==(Source const& other) const + ) const { - return token() == other.token(); + return forwardingSource_.forwardToRippled(request, forwardToRippledClientIp, yield); } - -protected: - util::Logger log_{"ETL"}; - -private: - friend etl::impl::ForwardCache; - friend ProbingSource; - - virtual std::optional - requestFromRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield - ) const = 0; }; -/** - * @brief Hooks for source events such as connects and disconnects. - */ -struct SourceHooks { - enum class Action { STOP, PROCEED }; +extern template class SourceImpl<>; - std::function onConnected; - std::function onDisconnected; -}; +using Source = SourceImpl<>; /** - * @brief Base implementation of shared source logic. + * @brief Create a source * - * @tparam Derived The derived class for CRTP + * @param config The configuration to use + * @param ioc The io_context to run on + * @param backend BackendInterface implementation + * @param subscriptions Subscription manager + * @param validatedLedgers The network validated ledgers data structure */ -template -class SourceImpl : public Source { - std::string wsPort_; - std::string grpcPort_; +Source +make_Source( + util::Config const& config, + boost::asio::io_context& ioc, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr validatedLedgers, + Source::OnDisconnectHook onDisconnect, + Source::OnConnectHook onConnect +); - std::vector> validatedLedgers_; - std::string validatedLedgersRaw_{"N/A"}; - std::shared_ptr networkValidatedLedgers_; - - mutable std::mutex mtx_; - - // true if this ETL source is forwarding transactions received on the transactions_proposed stream. There are - // usually multiple ETL sources, so to avoid forwarding the same transaction multiple times, we only forward from - // one particular ETL source at a time. - std::atomic_bool forwardingStream_{false}; - - std::chrono::system_clock::time_point lastMsgTime_; - mutable std::mutex lastMsgTimeMtx_; - - std::shared_ptr backend_; - std::shared_ptr subscriptions_; - LoadBalancer& balancer_; - - etl::impl::ForwardCache forwardCache_; - boost::uuids::uuid uuid_{}; - -protected: - std::string ip_; - size_t numFailures_ = 0; - - boost::asio::strand strand_; - boost::asio::steady_timer timer_; - boost::asio::ip::tcp::resolver resolver_; - boost::beast::flat_buffer readBuffer_; - - std::unique_ptr stub_; - - std::atomic_bool closing_{false}; - std::atomic_bool paused_{false}; - std::atomic_bool connected_{false}; - - SourceHooks hooks_; - -public: - /** - * @brief Create the base portion of ETL source. - * - * @param config The configuration to use - * @param ioc The io_context to run on - * @param backend BackendInterface implementation - * @param subscriptions Subscription manager - * @param validatedLedgers The network validated ledgers datastructure - * @param balancer Load balancer to use - * @param hooks Hooks to use for connect/disconnect events - */ - SourceImpl( - util::Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr validatedLedgers, - LoadBalancer& balancer, - SourceHooks hooks - ) - : networkValidatedLedgers_(std::move(validatedLedgers)) - , backend_(std::move(backend)) - , subscriptions_(std::move(subscriptions)) - , balancer_(balancer) - , forwardCache_(config, ioc, *this) - , strand_(boost::asio::make_strand(ioc)) - , timer_(strand_) - , resolver_(strand_) - , hooks_(std::move(hooks)) - { - static boost::uuids::random_generator uuidGenerator; - uuid_ = uuidGenerator(); - - ip_ = config.valueOr("ip", {}); - wsPort_ = config.valueOr("ws_port", {}); - - if (auto value = config.maybeValue("grpc_port"); value) { - grpcPort_ = *value; - try { - boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::make_address(ip_), std::stoi(grpcPort_)}; - std::stringstream ss; - ss << endpoint; - grpc::ChannelArguments chArgs; - chArgs.SetMaxReceiveMessageSize(-1); - stub_ = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub( - grpc::CreateCustomChannel(ss.str(), grpc::InsecureChannelCredentials(), chArgs) - ); - LOG(log_.debug()) << "Made stub for remote = " << toString(); - } catch (std::exception const& e) { - LOG(log_.debug()) << "Exception while creating stub = " << e.what() << " . Remote = " << toString(); - } - } - } - - ~SourceImpl() override - { - derived().close(false); - } - - bool - isConnected() const override - { - return connected_; - } - - boost::uuids::uuid - token() const override - { - return uuid_; - } - - std::optional - requestFromRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield - ) const override - { - LOG(log_.trace()) << "Attempting to forward request to tx. Request = " << boost::json::serialize(request); - - boost::json::object response; - - namespace beast = boost::beast; - namespace http = beast::http; - namespace websocket = beast::websocket; - namespace net = boost::asio; - using tcp = boost::asio::ip::tcp; - - try { - auto executor = boost::asio::get_associated_executor(yield); - beast::error_code ec; - tcp::resolver resolver{executor}; - - auto ws = std::make_unique>(executor); - - auto const results = resolver.async_resolve(ip_, wsPort_, yield[ec]); - if (ec) - return {}; - - ws->next_layer().expires_after(std::chrono::seconds(3)); - ws->next_layer().async_connect(results, yield[ec]); - if (ec) - return {}; - - // if client ip is know, change the User-Agent of the handshake and to tell rippled to charge the client - // IP for RPC resources. See "secure_gateway" in - // https://github.com/ripple/rippled/blob/develop/cfg/rippled-example.cfg - - // TODO: user-agent can be clio-[version] - ws->set_option(websocket::stream_base::decorator([&clientIp](websocket::request_type& req) { - req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); - if (clientIp) - req.set(http::field::forwarded, "for=" + *clientIp); - })); - - ws->async_handshake(ip_, "/", yield[ec]); - if (ec) - return {}; - - ws->async_write(net::buffer(boost::json::serialize(request)), yield[ec]); - if (ec) - return {}; - - beast::flat_buffer buffer; - ws->async_read(buffer, yield[ec]); - if (ec) - return {}; - - auto begin = static_cast(buffer.data().data()); - auto end = begin + buffer.data().size(); - auto parsed = boost::json::parse(std::string(begin, end)); - - if (!parsed.is_object()) { - LOG(log_.error()) << "Error parsing response: " << std::string{begin, end}; - return {}; - } - - response = parsed.as_object(); - response["forwarded"] = true; - - return response; - } catch (std::exception const& e) { - LOG(log_.error()) << "Encountered exception : " << e.what(); - return {}; - } - } - - bool - hasLedger(uint32_t sequence) const override - { - std::lock_guard const lck(mtx_); - for (auto& pair : validatedLedgers_) { - if (sequence >= pair.first && sequence <= pair.second) { - return true; - } - if (sequence < pair.first) { - // validatedLedgers_ is a sorted list of disjoint ranges - // if the sequence comes before this range, the sequence will - // come before all subsequent ranges - return false; - } - } - return false; - } - - std::pair - fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false) override - { - org::xrpl::rpc::v1::GetLedgerResponse response; - if (!stub_) - return {{grpc::StatusCode::INTERNAL, "No Stub"}, response}; - - // Ledger header with txns and metadata - org::xrpl::rpc::v1::GetLedgerRequest request; - grpc::ClientContext context; - - request.mutable_ledger()->set_sequence(sequence); - request.set_transactions(true); - request.set_expand(true); - request.set_get_objects(getObjects); - request.set_get_object_neighbors(getObjectNeighbors); - request.set_user("ETL"); - - grpc::Status const status = stub_->GetLedger(&context, request, &response); - - if (status.ok() && !response.is_unlimited()) { - log_.warn( - ) << "is_unlimited is false. Make sure secure_gateway is set correctly on the ETL source. source = " - << toString() << "; status = " << status.error_message(); - } - - return {status, std::move(response)}; - } - - std::string - toString() const override - { - return "{validated_ledger: " + getValidatedRange() + ", ip: " + ip_ + ", web socket port: " + wsPort_ + - ", grpc port: " + grpcPort_ + "}"; - } - - boost::json::object - toJson() const override - { - boost::json::object res; - - res["validated_range"] = getValidatedRange(); - res["is_connected"] = std::to_string(isConnected()); - res["ip"] = ip_; - res["ws_port"] = wsPort_; - res["grpc_port"] = grpcPort_; - - auto last = getLastMsgTime(); - if (last.time_since_epoch().count() != 0) { - res["last_msg_age_seconds"] = std::to_string( - std::chrono::duration_cast(std::chrono::system_clock::now() - getLastMsgTime()) - .count() - ); - } - - return res; - } - - std::pair, bool> - loadInitialLedger(std::uint32_t sequence, std::uint32_t numMarkers, bool cacheOnly = false) override - { - if (!stub_) - return {{}, false}; - - grpc::CompletionQueue cq; - void* tag = nullptr; - bool ok = false; - std::vector calls; - auto markers = getMarkers(numMarkers); - - for (size_t i = 0; i < markers.size(); ++i) { - std::optional nextMarker; - - if (i + 1 < markers.size()) - nextMarker = markers[i + 1]; - - calls.emplace_back(sequence, markers[i], nextMarker); - } - - LOG(log_.debug()) << "Starting data download for ledger " << sequence << ". Using source = " << toString(); - - for (auto& c : calls) - c.call(stub_, cq); - - size_t numFinished = 0; - bool abort = false; - size_t const incr = 500000; - size_t progress = incr; - std::vector edgeKeys; - - while (numFinished < calls.size() && cq.Next(&tag, &ok)) { - ASSERT(tag != nullptr, "Tag can't be null."); - auto ptr = static_cast(tag); - - if (!ok) { - LOG(log_.error()) << "loadInitialLedger - ok is false"; - return {{}, false}; // handle cancelled - } - - LOG(log_.trace()) << "Marker prefix = " << ptr->getMarkerPrefix(); - - auto result = ptr->process(stub_, cq, *backend_, abort, cacheOnly); - if (result != etl::impl::AsyncCallData::CallStatus::MORE) { - ++numFinished; - LOG(log_.debug()) << "Finished a marker. " - << "Current number of finished = " << numFinished; - - std::string const lastKey = ptr->getLastKey(); - - if (!lastKey.empty()) - edgeKeys.push_back(ptr->getLastKey()); - } - - if (result == etl::impl::AsyncCallData::CallStatus::ERRORED) - abort = true; - - if (backend_->cache().size() > progress) { - LOG(log_.info()) << "Downloaded " << backend_->cache().size() << " records from rippled"; - progress += incr; - } - } - - LOG(log_.info()) << "Finished loadInitialLedger. cache size = " << backend_->cache().size(); - return {std::move(edgeKeys), !abort}; - } - - std::optional - forwardToRippled( - boost::json::object const& request, - std::optional const& clientIp, - boost::asio::yield_context yield - ) const override - { - if (auto resp = forwardCache_.get(request); resp) { - LOG(log_.debug()) << "request hit forwardCache"; - return resp; - } - - return requestFromRippled(request, clientIp, yield); - } - - void - pause() override - { - paused_ = true; - derived().close(false); - } - - void - resume() override - { - paused_ = false; - derived().close(true); - } - - /** - * @brief Callback for resolving the server host. - * - * @param ec The error code - * @param results Result of the resolve operation - */ - void - onResolve(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results) - { - if (ec) { - // try again - reconnect(ec); - } else { - static constexpr std::size_t LOWEST_LAYER_TIMEOUT_SECONDS = 30; - boost::beast::get_lowest_layer(derived().ws()) - .expires_after(std::chrono::seconds(LOWEST_LAYER_TIMEOUT_SECONDS)); - boost::beast::get_lowest_layer(derived().ws()).async_connect(results, [this](auto ec, auto ep) { - derived().onConnect(ec, ep); - }); - } - } - - /** - * @brief Callback for handshake with the server. - * - * @param ec The error code - */ - void - onHandshake(boost::beast::error_code ec) - { - if (auto action = hooks_.onConnected(ec); action == SourceHooks::Action::STOP) - return; - - if (ec) { - // start over - reconnect(ec); - } else { - boost::json::object const jv{ - {"command", "subscribe"}, - {"streams", {"ledger", "manifests", "validations", "transactions_proposed"}}, - }; - std::string s = boost::json::serialize(jv); - LOG(log_.trace()) << "Sending subscribe stream message"; - - derived().ws().set_option( - boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) { - req.set( - boost::beast::http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " clio-client" - ); - req.set("X-User", "coro-client"); - }) - ); - - // Send subscription message - derived().ws().async_write(boost::asio::buffer(s), [this](auto ec, size_t size) { onWrite(ec, size); }); - } - } - - /** - * @brief Callback for writing data. - * - * @param ec The error code - * @param size Amount of bytes written - */ - void - onWrite(boost::beast::error_code ec, [[maybe_unused]] size_t size) - { - if (ec) { - reconnect(ec); - } else { - derived().ws().async_read(readBuffer_, [this](auto ec, size_t size) { onRead(ec, size); }); - } - } - - /** - * @brief Callback for data available to read. - * - * @param ec The error code - * @param size Amount of bytes read - */ - void - onRead(boost::beast::error_code ec, size_t size) - { - if (ec) { - reconnect(ec); - } else { - handleMessage(size); - derived().ws().async_read(readBuffer_, [this](auto ec, size_t size) { onRead(ec, size); }); - } - } - - /** - * @brief Handle the most recently received message. - * - * @param size Amount of bytes available in the read buffer - * @return true if the message was handled successfully; false otherwise - */ - bool - handleMessage(size_t size) - { - setLastMsgTime(); - - try { - auto const msg = boost::beast::buffers_to_string(readBuffer_.data()); - readBuffer_.consume(size); - - auto const raw = boost::json::parse(msg); - auto const response = raw.as_object(); - uint32_t ledgerIndex = 0; - - if (response.contains("result")) { - auto const& result = response.at("result").as_object(); - if (result.contains("ledger_index")) - ledgerIndex = result.at("ledger_index").as_int64(); - - if (result.contains("validated_ledgers")) { - auto const& validatedLedgers = result.at("validated_ledgers").as_string(); - setValidatedRange({validatedLedgers.data(), validatedLedgers.size()}); - } - - LOG(log_.info()) << "Received a message on ledger " - << " subscription stream. Message : " << response << " - " << toString(); - } else if (response.contains("type") && response.at("type") == "ledgerClosed") { - LOG(log_.info()) << "Received a message on ledger " - << " subscription stream. Message : " << response << " - " << toString(); - if (response.contains("ledger_index")) { - ledgerIndex = response.at("ledger_index").as_int64(); - } - if (response.contains("validated_ledgers")) { - auto const& validatedLedgers = response.at("validated_ledgers").as_string(); - setValidatedRange({validatedLedgers.data(), validatedLedgers.size()}); - } - } else { - if (balancer_.shouldPropagateTxnStream(this)) { - if (response.contains("transaction")) { - forwardCache_.freshen(); - subscriptions_->forwardProposedTransaction(response); - } else if (response.contains("type") && response.at("type") == "validationReceived") { - subscriptions_->forwardValidation(response); - } else if (response.contains("type") && response.at("type") == "manifestReceived") { - subscriptions_->forwardManifest(response); - } - } - } - - if (ledgerIndex != 0) { - LOG(log_.trace()) << "Pushing ledger sequence = " << ledgerIndex << " - " << toString(); - networkValidatedLedgers_->push(ledgerIndex); - } - - return true; - } catch (std::exception const& e) { - LOG(log_.error()) << "Exception in handleMessage : " << e.what(); - return false; - } - } - -protected: - Derived& - derived() - { - return static_cast(*this); - } - - void - run() override - { - resolver_.async_resolve(ip_, wsPort_, [this](auto ec, auto results) { onResolve(ec, results); }); - } - - void - reconnect(boost::beast::error_code ec) - { - static constexpr std::size_t BUFFER_SIZE = 128; - if (paused_) - return; - - if (isConnected()) - hooks_.onDisconnected(ec); - - connected_ = false; - readBuffer_ = {}; - - // These are somewhat normal errors. operation_aborted occurs on shutdown, - // when the timer is cancelled. connection_refused will occur repeatedly - std::string err = ec.message(); - // if we cannot connect to the transaction processing process - if (ec.category() == boost::asio::error::get_ssl_category()) { - err = std::string(" (") + boost::lexical_cast(ERR_GET_LIB(ec.value())) + "," + - boost::lexical_cast(ERR_GET_REASON(ec.value())) + ") "; - - // ERR_PACK /* crypto/err/err.h */ - char buf[BUFFER_SIZE]; - ::ERR_error_string_n(ec.value(), buf, sizeof(buf)); - err += buf; - - LOG(log_.error()) << err; - } - - if (ec != boost::asio::error::operation_aborted && ec != boost::asio::error::connection_refused) { - LOG(log_.error()) << "error code = " << ec << " - " << toString(); - } else { - LOG(log_.warn()) << "error code = " << ec << " - " << toString(); - } - - // exponentially increasing timeouts, with a max of 30 seconds - size_t const waitTime = std::min(pow(2, numFailures_), 30.0); - numFailures_++; - timer_.expires_after(boost::asio::chrono::seconds(waitTime)); - timer_.async_wait([this](auto ec) { - bool const startAgain = (ec != boost::asio::error::operation_aborted); - derived().close(startAgain); - }); - } - -private: - void - setLastMsgTime() - { - std::lock_guard const lck(lastMsgTimeMtx_); - lastMsgTime_ = std::chrono::system_clock::now(); - } - - std::chrono::system_clock::time_point - getLastMsgTime() const - { - std::lock_guard const lck(lastMsgTimeMtx_); - return lastMsgTime_; - } - - void - setValidatedRange(std::string const& range) - { - std::vector> pairs; - std::vector ranges; - boost::split(ranges, range, boost::is_any_of(",")); - for (auto& pair : ranges) { - std::vector minAndMax; - - boost::split(minAndMax, pair, boost::is_any_of("-")); - - if (minAndMax.size() == 1) { - uint32_t const sequence = std::stoll(minAndMax[0]); - pairs.emplace_back(sequence, sequence); - } else { - ASSERT(minAndMax.size() == 2, "Min and max should be of size 2. Got size = {}", minAndMax.size()); - uint32_t const min = std::stoll(minAndMax[0]); - uint32_t const max = std::stoll(minAndMax[1]); - pairs.emplace_back(min, max); - } - } - std::sort(pairs.begin(), pairs.end(), [](auto left, auto right) { return left.first < right.first; }); - - // we only hold the lock here, to avoid blocking while string processing - std::lock_guard const lck(mtx_); - validatedLedgers_ = std::move(pairs); - validatedLedgersRaw_ = range; - } - - std::string - getValidatedRange() const - { - std::lock_guard const lck(mtx_); - return validatedLedgersRaw_; - } -}; - -/** - * @brief Implementation of a source that uses a regular, non-secure websocket connection. - */ -class PlainSource : public SourceImpl { - using StreamType = boost::beast::websocket::stream; - std::unique_ptr ws_; - -public: - /** - * @brief Create a non-secure ETL source. - * - * @param config The configuration to use - * @param ioc The io_context to run on - * @param backend BackendInterface implementation - * @param subscriptions Subscription manager - * @param validatedLedgers The network validated ledgers datastructure - * @param balancer Load balancer to use - * @param hooks Hooks to use for connect/disconnect events - */ - PlainSource( - util::Config const& config, - boost::asio::io_context& ioc, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr validatedLedgers, - LoadBalancer& balancer, - SourceHooks hooks - ) - : SourceImpl(config, ioc, backend, subscriptions, validatedLedgers, balancer, std::move(hooks)) - , ws_(std::make_unique(strand_)) - { - } - - /** - * @brief Callback for connection to the server. - * - * @param ec The error code - * @param endpoint The resolved endpoint - */ - void - onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint); - - /** - * @brief Close the websocket. - * - * @param startAgain Whether to automatically reconnect - */ - void - close(bool startAgain); - - /** @return The underlying TCP stream */ - StreamType& - ws() - { - return *ws_; - } -}; - -/** - * @brief Implementation of a source that uses a secure websocket connection. - */ -class SslSource : public SourceImpl { - using StreamType = boost::beast::websocket::stream>; - std::optional> sslCtx_; - std::unique_ptr ws_; - -public: - /** - * @brief Create a secure ETL source. - * - * @param config The configuration to use - * @param ioc The io_context to run on - * @param sslCtx The SSL context if any - * @param backend BackendInterface implementation - * @param subscriptions Subscription manager - * @param validatedLedgers The network validated ledgers datastructure - * @param balancer Load balancer to use - * @param hooks Hooks to use for connect/disconnect events - */ - SslSource( - util::Config const& config, - boost::asio::io_context& ioc, - std::optional> sslCtx, - std::shared_ptr backend, - std::shared_ptr subscriptions, - std::shared_ptr validatedLedgers, - LoadBalancer& balancer, - SourceHooks hooks - ) - : SourceImpl(config, ioc, backend, subscriptions, validatedLedgers, balancer, std::move(hooks)) - , sslCtx_(sslCtx) - , ws_(std::make_unique(strand_, *sslCtx_)) - { - } - - /** - * @brief Callback for connection to the server. - * - * @param ec The error code - * @param endpoint The resolved endpoint - */ - void - onConnect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint); - - /** - * @brief Callback for SSL handshake completion. - * - * @param ec The error code - * @param endpoint The resolved endpoint - */ - void - onSslHandshake(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint); - - /** - * @brief Close the websocket. - * - * @param startAgain Whether to automatically reconnect - */ - void - close(bool startAgain); - - /** @return The underlying SSL stream */ - StreamType& - ws() - { - return *ws_; - } -}; } // namespace etl diff --git a/src/etl/impl/AsyncData.hpp b/src/etl/impl/AsyncData.hpp index 08715604..422bb2ef 100644 --- a/src/etl/impl/AsyncData.hpp +++ b/src/etl/impl/AsyncData.hpp @@ -21,6 +21,7 @@ #include "data/BackendInterface.hpp" #include "data/Types.hpp" +#include "etl/ETLHelpers.hpp" #include "etl/NFTHelpers.hpp" #include "util/Assert.hpp" #include "util/log/Logger.hpp" @@ -33,6 +34,7 @@ #include #include +#include #include #include #include @@ -145,7 +147,7 @@ public: continue; } cacheUpdates.push_back( - {*ripple::uint256::fromVoidChecked(obj.key()), {obj.mutable_data()->begin(), obj.mutable_data()->end()}} + {*ripple::uint256::fromVoidChecked(obj.key()), {obj.data().begin(), obj.data().end()}} ); if (!cacheOnly) { if (!lastKey_.empty()) @@ -193,4 +195,21 @@ public: } }; +inline std::vector +makeAsyncCallData(uint32_t const sequence, uint32_t const numMarkers) +{ + auto const markers = getMarkers(numMarkers); + + std::vector result; + result.reserve(markers.size()); + + for (size_t i = 0; i + 1 < markers.size(); ++i) { + result.emplace_back(sequence, markers[i], markers[i + 1]); + } + if (not markers.empty()) { + result.emplace_back(sequence, markers.back(), std::nullopt); + } + return result; +} + } // namespace etl::impl diff --git a/src/etl/impl/ForwardCache.cpp b/src/etl/impl/ForwardCache.cpp deleted file mode 100644 index 3eb9697d..00000000 --- a/src/etl/impl/ForwardCache.cpp +++ /dev/null @@ -1,95 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2022, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "etl/impl/ForwardCache.hpp" - -#include "etl/Source.hpp" -#include "rpc/RPCHelpers.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace etl::impl { - -void -ForwardCache::freshen() -{ - LOG(log_.trace()) << "Freshening ForwardCache"; - - auto numOutstanding = std::make_shared(latestForwarded_.size()); - - for (auto const& cacheEntry : latestForwarded_) { - boost::asio::spawn( - strand_, - [this, numOutstanding, command = cacheEntry.first](boost::asio::yield_context yield) { - boost::json::object const request = {{"command", command}}; - auto resp = source_.requestFromRippled(request, std::nullopt, yield); - - if (!resp || resp->contains("error")) - resp = {}; - - { - std::scoped_lock const lk(mtx_); - latestForwarded_[command] = resp; - } - } - ); - } -} - -void -ForwardCache::clear() -{ - std::scoped_lock const lk(mtx_); - for (auto& cacheEntry : latestForwarded_) - latestForwarded_[cacheEntry.first] = {}; -} - -std::optional -ForwardCache::get(boost::json::object const& request) const -{ - std::optional command = {}; - if (request.contains("command") && !request.contains("method") && request.at("command").is_string()) { - command = boost::json::value_to(request.at("command")); - } else if (request.contains("method") && !request.contains("command") && request.at("method").is_string()) { - command = boost::json::value_to(request.at("method")); - } - - if (!command) - return {}; - if (rpc::specifiesCurrentOrClosedLedger(request)) - return {}; - - std::shared_lock const lk(mtx_); - if (!latestForwarded_.contains(*command)) - return {}; - - return {latestForwarded_.at(*command)}; -} - -} // namespace etl::impl diff --git a/src/etl/impl/ForwardCache.hpp b/src/etl/impl/ForwardCache.hpp deleted file mode 100644 index 5914a238..00000000 --- a/src/etl/impl/ForwardCache.hpp +++ /dev/null @@ -1,85 +0,0 @@ -//------------------------------------------------------------------------------ -/* - 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 "util/config/Config.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace etl { -class Source; -} // namespace etl - -namespace etl::impl { - -/** - * @brief Cache for rippled responses - */ -class ForwardCache { - using ResponseType = std::optional; - static constexpr std::uint32_t DEFAULT_DURATION = 10; - - util::Logger log_{"ETL"}; - - mutable std::shared_mutex mtx_; - std::unordered_map latestForwarded_; - boost::asio::strand strand_; - etl::Source const& source_; - std::uint32_t duration_ = DEFAULT_DURATION; - - void - clear(); - -public: - ForwardCache(util::Config const& config, boost::asio::io_context& ioc, Source const& source) - : strand_(boost::asio::make_strand(ioc)), source_(source) - { - if (config.contains("cache")) { - auto commands = config.arrayOrThrow("cache", "Source cache must be array"); - - if (config.contains("cache_duration")) - duration_ = config.valueOrThrow("cache_duration", "Source cache_duration must be a number"); - - for (auto const& command : commands) { - auto key = command.valueOrThrow("Source forward command must be array of strings"); - latestForwarded_[key] = {}; - } - } - } - - void - freshen(); - - std::optional - get(boost::json::object const& request) const; -}; - -} // namespace etl::impl diff --git a/src/etl/impl/ForwardingSource.cpp b/src/etl/impl/ForwardingSource.cpp new file mode 100644 index 00000000..0d4487af --- /dev/null +++ b/src/etl/impl/ForwardingSource.cpp @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +/* + 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/ForwardingSource.hpp" + +#include "util/log/Logger.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace etl::impl { + +ForwardingSource::ForwardingSource( + std::string ip, + std::string wsPort, + std::chrono::steady_clock::duration connectionTimeout +) + : log_(fmt::format("ForwardingSource[{}:{}]", ip, wsPort)), connectionBuilder_(std::move(ip), std::move(wsPort)) +{ + connectionBuilder_.setConnectionTimeout(connectionTimeout) + .addHeader( + {boost::beast::http::field::user_agent, fmt::format("{} websocket-client-coro", BOOST_BEAST_VERSION_STRING)} + ); +} + +std::optional +ForwardingSource::forwardToRippled( + boost::json::object const& request, + std::optional const& forwardToRippledClientIp, + boost::asio::yield_context yield +) const +{ + auto connectionBuilder = connectionBuilder_; + if (forwardToRippledClientIp) { + connectionBuilder.addHeader( + {boost::beast::http::field::forwarded, fmt::format("for={}", *forwardToRippledClientIp)} + ); + } + auto expectedConnection = connectionBuilder.connect(yield); + if (not expectedConnection) { + return std::nullopt; + } + auto& connection = expectedConnection.value(); + + auto writeError = connection->write(boost::json::serialize(request), yield); + if (writeError) { + return std::nullopt; + } + + auto response = connection->read(yield); + if (not response) { + return std::nullopt; + } + + boost::json::value parsedResponse; + try { + parsedResponse = boost::json::parse(*response); + if (not parsedResponse.is_object()) + throw std::runtime_error("response is not an object"); + } catch (std::exception const& e) { + LOG(log_.error()) << "Error parsing response from rippled: " << e.what() << ". Response: " << *response; + return std::nullopt; + } + + auto responseObject = parsedResponse.as_object(); + responseObject["forwarded"] = true; + return responseObject; +} + +} // namespace etl::impl diff --git a/src/etl/impl/ForwardingSource.hpp b/src/etl/impl/ForwardingSource.hpp new file mode 100644 index 00000000..0112809e --- /dev/null +++ b/src/etl/impl/ForwardingSource.hpp @@ -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. +*/ +//============================================================================== + +#pragma once + +#include "util/log/Logger.hpp" +#include "util/requests/WsConnection.hpp" + +#include +#include + +#include +#include +#include + +namespace etl::impl { + +class ForwardingSource { + util::Logger log_; + util::requests::WsConnectionBuilder connectionBuilder_; + static constexpr std::chrono::seconds CONNECTION_TIMEOUT{3}; + +public: + ForwardingSource( + std::string ip_, + std::string wsPort_, + std::chrono::steady_clock::duration connectionTimeout = CONNECTION_TIMEOUT + ); + + /** + * @brief Forward a request to rippled. + * + * @param request The request to forward + * @param clientIp 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 + forwardToRippled( + boost::json::object const& request, + std::optional const& forwardToRippledClientIp, + boost::asio::yield_context yield + ) const; +}; + +} // namespace etl::impl diff --git a/src/etl/impl/GrpcSource.cpp b/src/etl/impl/GrpcSource.cpp new file mode 100644 index 00000000..940b647d --- /dev/null +++ b/src/etl/impl/GrpcSource.cpp @@ -0,0 +1,151 @@ +//------------------------------------------------------------------------------ +/* + 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/GrpcSource.hpp" + +#include "data/BackendInterface.hpp" +#include "etl/impl/AsyncData.hpp" +#include "util/Assert.hpp" +#include "util/log/Logger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace etl::impl { + +GrpcSource::GrpcSource(std::string const& ip, std::string const& grpcPort, std::shared_ptr backend) + : log_(fmt::format("ETL_Grpc[{}:{}]", ip, grpcPort)), backend_(std::move(backend)) +{ + try { + boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::make_address(ip), std::stoi(grpcPort)}; + std::stringstream ss; + ss << endpoint; + grpc::ChannelArguments chArgs; + chArgs.SetMaxReceiveMessageSize(-1); + stub_ = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub( + grpc::CreateCustomChannel(ss.str(), grpc::InsecureChannelCredentials(), chArgs) + ); + LOG(log_.debug()) << "Made stub for remote."; + } catch (std::exception const& e) { + LOG(log_.warn()) << "Exception while creating stub: " << e.what() << "."; + } +} + +std::pair +GrpcSource::fetchLedger(uint32_t sequence, bool getObjects, bool getObjectNeighbors) +{ + org::xrpl::rpc::v1::GetLedgerResponse response; + if (!stub_) + return {{grpc::StatusCode::INTERNAL, "No Stub"}, response}; + + // Ledger header with txns and metadata + org::xrpl::rpc::v1::GetLedgerRequest request; + grpc::ClientContext context; + + request.mutable_ledger()->set_sequence(sequence); + request.set_transactions(true); + request.set_expand(true); + request.set_get_objects(getObjects); + request.set_get_object_neighbors(getObjectNeighbors); + request.set_user("ETL"); + + grpc::Status const status = stub_->GetLedger(&context, request, &response); + + if (status.ok() && !response.is_unlimited()) { + log_.warn() << "is_unlimited is false. Make sure secure_gateway is set correctly on the ETL source. Status = " + << status.error_message(); + } + + return {status, std::move(response)}; +} + +std::pair, bool> +GrpcSource::loadInitialLedger(uint32_t const sequence, uint32_t const numMarkers, bool const cacheOnly) +{ + if (!stub_) + return {{}, false}; + + std::vector calls = impl::makeAsyncCallData(sequence, numMarkers); + + LOG(log_.debug()) << "Starting data download for ledger " << sequence << "."; + + grpc::CompletionQueue cq; + for (auto& c : calls) + c.call(stub_, cq); + + void* tag = nullptr; + bool ok = false; + size_t numFinished = 0; + bool abort = false; + size_t const incr = 500000; + size_t progress = incr; + std::vector edgeKeys; + + while (numFinished < calls.size() && cq.Next(&tag, &ok)) { + ASSERT(tag != nullptr, "Tag can't be null."); + auto ptr = static_cast(tag); + + if (!ok) { + LOG(log_.error()) << "loadInitialLedger - ok is false"; + return {{}, false}; // handle cancelled + } + + LOG(log_.trace()) << "Marker prefix = " << ptr->getMarkerPrefix(); + + auto result = ptr->process(stub_, cq, *backend_, abort, cacheOnly); + if (result != etl::impl::AsyncCallData::CallStatus::MORE) { + ++numFinished; + LOG(log_.debug()) << "Finished a marker. " + << "Current number of finished = " << numFinished; + + if (auto lastKey = ptr->getLastKey(); !lastKey.empty()) + edgeKeys.push_back(std::move(lastKey)); + } + + if (result == etl::impl::AsyncCallData::CallStatus::ERRORED) + abort = true; + + if (backend_->cache().size() > progress) { + LOG(log_.info()) << "Downloaded " << backend_->cache().size() << " records from rippled"; + progress += incr; + } + } + + LOG(log_.info()) << "Finished loadInitialLedger. cache size = " << backend_->cache().size() << ", abort = " << abort + << "."; + return {std::move(edgeKeys), !abort}; +} + +} // namespace etl::impl diff --git a/src/etl/impl/GrpcSource.hpp b/src/etl/impl/GrpcSource.hpp new file mode 100644 index 00000000..3e45adc0 --- /dev/null +++ b/src/etl/impl/GrpcSource.hpp @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +/* + 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/BackendInterface.hpp" +#include "util/log/Logger.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +namespace etl::impl { + +class GrpcSource { + util::Logger log_; + std::unique_ptr stub_; + std::shared_ptr backend_; + +public: + GrpcSource(std::string const& ip, std::string const& grpcPort, std::shared_ptr backend); + + /** + * @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 + fetchLedger(uint32_t sequence, bool getObjects = true, bool getObjectNeighbors = false); + + /** + * @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, bool> + loadInitialLedger(uint32_t sequence, uint32_t numMarkers, bool cacheOnly = false); +}; + +} // namespace etl::impl diff --git a/src/etl/impl/SubscriptionSource.cpp b/src/etl/impl/SubscriptionSource.cpp new file mode 100644 index 00000000..369cd8f3 --- /dev/null +++ b/src/etl/impl/SubscriptionSource.cpp @@ -0,0 +1,320 @@ +//------------------------------------------------------------------------------ +/* + 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/SubscriptionSource.hpp" + +#include "rpc/JS.hpp" +#include "util/Expected.hpp" +#include "util/Retry.hpp" +#include "util/log/Logger.hpp" +#include "util/requests/Types.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace etl::impl { + +SubscriptionSource::~SubscriptionSource() +{ + stop(); + retry_.cancel(); + + if (runFuture_.valid()) + runFuture_.wait(); +} + +void +SubscriptionSource::run() +{ + subscribe(); +} + +bool +SubscriptionSource::hasLedger(uint32_t sequence) const +{ + auto validatedLedgersData = validatedLedgersData_.lock(); + for (auto& pair : validatedLedgersData->validatedLedgers) { + if (sequence >= pair.first && sequence <= pair.second) { + return true; + } + if (sequence < pair.first) { + // validatedLedgers_ is a sorted list of disjoint ranges + // if the sequence comes before this range, the sequence will + // come before all subsequent ranges + return false; + } + } + return false; +} + +bool +SubscriptionSource::isConnected() const +{ + return isConnected_; +} + +void +SubscriptionSource::setForwarding(bool isForwarding) +{ + isForwarding_ = isForwarding; +} + +std::chrono::steady_clock::time_point +SubscriptionSource::lastMessageTime() const +{ + return lastMessageTime_.lock().get(); +} + +std::string const& +SubscriptionSource::validatedRange() const +{ + return validatedLedgersData_.lock()->validatedLedgersRaw; +} + +void +SubscriptionSource::stop() +{ + stop_ = true; +} + +void +SubscriptionSource::subscribe() +{ + runFuture_ = boost::asio::spawn( + strand_, + [this, _ = boost::asio::make_work_guard(strand_)](boost::asio::yield_context yield) { + auto connection = wsConnectionBuilder_.connect(yield); + if (not connection) { + handleError(connection.error(), yield); + return; + } + + wsConnection_ = std::move(connection).value(); + isConnected_ = true; + onConnect_(); + + auto const& subscribeCommand = getSubscribeCommandJson(); + auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield); + if (writeErrorOpt) { + handleError(writeErrorOpt.value(), yield); + return; + } + + retry_.reset(); + + while (!stop_) { + auto const message = wsConnection_->read(yield); + if (not message) { + handleError(message.error(), yield); + return; + } + + auto const handleErrorOpt = handleMessage(message.value()); + if (handleErrorOpt) { + handleError(handleErrorOpt.value(), yield); + return; + } + } + // Close the connection + handleError( + util::requests::RequestError{"Subscription source stopped", boost::asio::error::operation_aborted}, + yield + ); + }, + boost::asio::use_future + ); +} + +std::optional +SubscriptionSource::handleMessage(std::string const& message) +{ + setLastMessageTime(); + + try { + auto const raw = boost::json::parse(message); + auto const object = raw.as_object(); + uint32_t ledgerIndex = 0; + + static constexpr char const* const JS_LedgerClosed = "ledgerClosed"; + static constexpr char const* const JS_ValidationReceived = "validationReceived"; + static constexpr char const* const JS_ManifestReceived = "manifestReceived"; + + if (object.contains(JS(result))) { + auto const& result = object.at(JS(result)).as_object(); + if (result.contains(JS(ledger_index))) + ledgerIndex = result.at(JS(ledger_index)).as_int64(); + + if (result.contains(JS(validated_ledgers))) { + auto validatedLedgers = boost::json::value_to(result.at(JS(validated_ledgers))); + setValidatedRange(std::move(validatedLedgers)); + } + LOG(log_.info()) << "Received a message on ledger subscription stream. Message : " << object; + + } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_LedgerClosed) { + LOG(log_.info()) << "Received a message on ledger subscription stream. Message : " << object; + if (object.contains(JS(ledger_index))) { + ledgerIndex = object.at(JS(ledger_index)).as_int64(); + } + if (object.contains(JS(validated_ledgers))) { + auto validatedLedgers = boost::json::value_to(object.at(JS(validated_ledgers))); + setValidatedRange(std::move(validatedLedgers)); + } + + } else { + if (isForwarding_) { + if (object.contains(JS(transaction))) { + dependencies_.forwardProposedTransaction(object); + } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ValidationReceived) { + dependencies_.forwardValidation(object); + } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ManifestReceived) { + dependencies_.forwardManifest(object); + } + } + } + + if (ledgerIndex != 0) { + LOG(log_.trace()) << "Pushing ledger sequence = " << ledgerIndex; + dependencies_.pushValidatedLedger(ledgerIndex); + } + + return std::nullopt; + } catch (std::exception const& e) { + LOG(log_.error()) << "Exception in handleMessage : " << e.what(); + return util::requests::RequestError{fmt::format("Error handling message: {}", e.what())}; + } +} + +void +SubscriptionSource::handleError(util::requests::RequestError const& error, boost::asio::yield_context yield) +{ + isConnected_ = false; + if (not stop_) { + onDisconnect_(); + isForwarding_ = false; + } + + if (wsConnection_ != nullptr) { + auto const error = wsConnection_->close(yield); + if (error) { + LOG(log_.error()) << "Error closing websocket connection: " << error->message(); + } + wsConnection_.reset(); + } + + logError(error); + if (not stop_) { + retry_.retry([this] { subscribe(); }); + } +} + +void +SubscriptionSource::logError(util::requests::RequestError const& error) const +{ + auto const& errorCodeOpt = error.errorCode(); + + if (not errorCodeOpt or + (errorCodeOpt.value() != boost::asio::error::operation_aborted && + errorCodeOpt.value() != boost::asio::error::connection_refused)) { + LOG(log_.error()) << error.message(); + } else { + LOG(log_.warn()) << error.message(); + } +} + +void +SubscriptionSource::setLastMessageTime() +{ + lastMessageTime_.lock().get() = std::chrono::steady_clock::now(); +} + +void +SubscriptionSource::setValidatedRange(std::string range) +{ + std::vector ranges; + boost::split(ranges, range, [](char const c) { return c == ','; }); + + std::vector> pairs; + pairs.reserve(ranges.size()); + for (auto& pair : ranges) { + std::vector minAndMax; + + boost::split(minAndMax, pair, boost::is_any_of("-")); + + if (minAndMax.size() == 1) { + uint32_t const sequence = std::stoll(minAndMax[0]); + pairs.emplace_back(sequence, sequence); + } else { + if (minAndMax.size() != 2) { + throw std::runtime_error(fmt::format( + "Error parsing range: {}.Min and max should be of size 2. Got size = {}", range, minAndMax.size() + )); + } + uint32_t const min = std::stoll(minAndMax[0]); + uint32_t const max = std::stoll(minAndMax[1]); + pairs.emplace_back(min, max); + } + } + std::sort(pairs.begin(), pairs.end(), [](auto left, auto right) { return left.first < right.first; }); + + auto dataLock = validatedLedgersData_.lock(); + dataLock->validatedLedgers = std::move(pairs); + dataLock->validatedLedgersRaw = std::move(range); +} + +std::string const& +SubscriptionSource::getSubscribeCommandJson() +{ + static boost::json::object const jsonValue{ + {"command", "subscribe"}, + {"streams", {"ledger", "manifests", "validations", "transactions_proposed"}}, + }; + static std::string const jsonString = boost::json::serialize(jsonValue); + return jsonString; +} + +} // namespace etl::impl diff --git a/src/etl/impl/SubscriptionSource.hpp b/src/etl/impl/SubscriptionSource.hpp new file mode 100644 index 00000000..501d462e --- /dev/null +++ b/src/etl/impl/SubscriptionSource.hpp @@ -0,0 +1,213 @@ +//------------------------------------------------------------------------------ +/* + 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/impl/SubscriptionSourceDependencies.hpp" +#include "util/Mutex.hpp" +#include "util/Retry.hpp" +#include "util/log/Logger.hpp" +#include "util/requests/Types.hpp" +#include "util/requests/WsConnection.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace etl::impl { + +/** + * @brief This class is used to subscribe to a source of ledger data and forward it to the subscription manager. + */ +class SubscriptionSource { +public: + using OnConnectHook = std::function; + using OnDisconnectHook = std::function; + +private: + util::Logger log_; + util::requests::WsConnectionBuilder wsConnectionBuilder_; + util::requests::WsConnectionPtr wsConnection_; + + struct ValidatedLedgersData { + std::vector> validatedLedgers; + std::string validatedLedgersRaw{"N/A"}; + }; + util::Mutex validatedLedgersData_; + + SubscriptionSourceDependencies dependencies_; + + boost::asio::strand strand_; + + util::Retry retry_; + + OnConnectHook onConnect_; + OnDisconnectHook onDisconnect_; + + std::atomic_bool isConnected_{false}; + std::atomic_bool stop_{false}; + std::atomic_bool isForwarding_{false}; + + util::Mutex lastMessageTime_; + + std::future runFuture_; + + static constexpr std::chrono::seconds CONNECTION_TIMEOUT{30}; + static constexpr std::chrono::seconds RETRY_MAX_DELAY{30}; + static constexpr std::chrono::seconds RETRY_DELAY{1}; + +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 + * @param validatedLedgers The network validated ledgers object + * @param subscriptions The subscription manager object + * @param onDisconnect The onDisconnect hook. Called when the connection is lost + * @param connectionTimeout The connection timeout. Defaults to 30 seconds + * @param retryDelay The retry delay. Defaults to 1 second + */ + template + SubscriptionSource( + boost::asio::io_context& ioContext, + std::string const& ip, + std::string const& wsPort, + std::shared_ptr validatedLedgers, + std::shared_ptr subscriptions, + OnConnectHook onConnect, + OnDisconnectHook onDisconnect, + 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)) + { + wsConnectionBuilder_.addHeader({boost::beast::http::field::user_agent, "clio-client"}) + .addHeader({"X-User", "clio-client"}) + .setConnectionTimeout(connectionTimeout); + } + + /** + * @brief Destroy the Subscription Source object + * + * @note This will block to wait for all the async operations to complete. io_context must be still running + */ + ~SubscriptionSource(); + + /** + * @brief Run the source + */ + void + run(); + + /** + * @brief Check if the source has a ledger + * + * @param sequence The sequence of the ledger + * @return true if the source has the ledger, false otherwise + */ + bool + hasLedger(uint32_t sequence) const; + + /** + * @brief Check if the source is connected + * + * @return true if the source is connected, false otherwise + */ + bool + isConnected() const; + + /** + * @brief Set source forwarding + * + * @note If forwarding is true the source will forward messages to the subscription manager. Forwarding is being + * reset on disconnect. + * @param isForwarding The new forwarding state + */ + void + setForwarding(bool isForwarding); + + /** + * @brief Get the last message time (even if the last message had an error) + * + * @return The last message time + */ + std::chrono::steady_clock::time_point + lastMessageTime() const; + + /** + * @brief Get the last received raw string of the validated ledgers + * + * @return The validated ledgers string + */ + std::string const& + validatedRange() const; + + /** + * @brief Stop the source. The source will complete already scheduled operations but will not schedule new ones + */ + void + stop(); + +private: + void + subscribe(); + + std::optional + handleMessage(std::string const& message); + + void + handleError(util::requests::RequestError const& error, boost::asio::yield_context yield); + + void + logError(util::requests::RequestError const& error) const; + + void + setLastMessageTime(); + + void + setValidatedRange(std::string range); + + static std::string const& + getSubscribeCommandJson(); +}; + +} // namespace etl::impl diff --git a/src/etl/impl/SubscriptionSourceDependencies.hpp b/src/etl/impl/SubscriptionSourceDependencies.hpp new file mode 100644 index 00000000..810b92c3 --- /dev/null +++ b/src/etl/impl/SubscriptionSourceDependencies.hpp @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include + +namespace etl::impl { + +class SubscriptionSourceDependencies { + struct Concept; + std::unique_ptr pImpl_; + +public: + template + SubscriptionSourceDependencies( + std::shared_ptr networkValidatedLedgers, + std::shared_ptr subscriptions + ) + : pImpl_{std::make_unique>( + 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 + class Model : public Concept { + std::shared_ptr networkValidatedLedgers_; + std::shared_ptr subscriptions_; + + public: + Model( + std::shared_ptr networkValidatedLedgers, + std::shared_ptr 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 diff --git a/src/main/Main.cpp b/src/main/Main.cpp index 1f76fc75..2f17348c 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -160,6 +160,8 @@ start(io_context& ioc, std::uint32_t numThreads) v.emplace_back([&ioc] { ioc.run(); }); ioc.run(); + for (auto& t : v) + t.join(); } int diff --git a/src/util/Mutex.hpp b/src/util/Mutex.hpp new file mode 100644 index 00000000..1e3eb375 --- /dev/null +++ b/src/util/Mutex.hpp @@ -0,0 +1,122 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace util { + +template +class Mutex; + +/** + * @brief A lock on a mutex that provides access to the protected data. + * + * @tparam ProtectedDataType data type to hold + */ +template +class Lock { + std::scoped_lock lock_; + ProtectedDataType& data_; + +public: + ProtectedDataType const& + operator*() const + { + return data_; + } + + ProtectedDataType& + operator*() + { + return data_; + } + + ProtectedDataType const& + get() const + { + return data_; + } + + ProtectedDataType& + get() + { + return data_; + } + + ProtectedDataType const* + operator->() const + { + return &data_; + } + + ProtectedDataType* + operator->() + { + return &data_; + } + +private: + friend class Mutex>; + + explicit Lock(std::mutex& mutex, ProtectedDataType& data) : lock_(mutex), data_(data) + { + } +}; + +/** + * @brief A container for data that is protected by a mutex. Inspired by Mutex in Rust. + * + * @tparam ProtectedDataType data type to hold + */ +template +class Mutex { + mutable std::mutex mutex_; + ProtectedDataType data_; + +public: + Mutex() = default; + + explicit Mutex(ProtectedDataType data) : data_(std::move(data)) + { + } + + template + static Mutex + make(Args&&... args) + { + return Mutex{ProtectedDataType{std::forward(args)...}}; + } + + Lock + lock() const + { + return Lock{mutex_, data_}; + } + + Lock + lock() + { + return Lock{mutex_, data_}; + } +}; + +} // namespace util diff --git a/src/util/Retry.cpp b/src/util/Retry.cpp new file mode 100644 index 00000000..41326cdc --- /dev/null +++ b/src/util/Retry.cpp @@ -0,0 +1,115 @@ +//------------------------------------------------------------------------------ +/* + 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/Retry.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace util { + +RetryStrategy::RetryStrategy(std::chrono::steady_clock::duration delay) : initialDelay_(delay), delay_(delay) +{ +} + +std::chrono::steady_clock::duration +RetryStrategy::getDelay() const +{ + return delay_; +} + +void +RetryStrategy::increaseDelay() +{ + delay_ = nextDelay(); +} + +void +RetryStrategy::reset() +{ + delay_ = initialDelay_; +} + +Retry::Retry(RetryStrategyPtr strategy, boost::asio::strand strand) + : strategy_(std::move(strategy)), timer_(strand.get_inner_executor()) +{ +} + +Retry::~Retry() +{ + cancel(); +} + +void +Retry::cancel() +{ + timer_.cancel(); +} + +size_t +Retry::attemptNumber() const +{ + return attemptNumber_; +} + +std::chrono::steady_clock::duration +Retry::delayValue() const +{ + return strategy_->getDelay(); +} + +void +Retry::reset() +{ + attemptNumber_ = 0; + strategy_->reset(); +} + +ExponentialBackoffStrategy::ExponentialBackoffStrategy( + std::chrono::steady_clock::duration delay, + std::chrono::steady_clock::duration maxDelay +) + : RetryStrategy(delay), maxDelay_(maxDelay) +{ +} + +std::chrono::steady_clock::duration +ExponentialBackoffStrategy::nextDelay() const +{ + auto const next = getDelay() * 2; + return std::min(next, maxDelay_); +} + +Retry +makeRetryExponentialBackoff( + std::chrono::steady_clock::duration delay, + std::chrono::steady_clock::duration maxDelay, + boost::asio::strand strand +) +{ + return Retry(std::make_unique(delay, maxDelay), std::move(strand)); +} + +} // namespace util diff --git a/src/util/Retry.hpp b/src/util/Retry.hpp new file mode 100644 index 00000000..edcdc727 --- /dev/null +++ b/src/util/Retry.hpp @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +#include +#include +#include + +namespace util { + +/** + * @brief Interface for retry strategies + */ +class RetryStrategy { + std::chrono::steady_clock::duration initialDelay_; + std::chrono::steady_clock::duration delay_; + +public: + RetryStrategy(std::chrono::steady_clock::duration delay); + virtual ~RetryStrategy() = default; + + /** + * @brief Get the current delay value + * + * @return std::chrono::steady_clock::duration + */ + std::chrono::steady_clock::duration + getDelay() const; + + /** + * @brief Increase the delay value + */ + void + increaseDelay(); + + /** + * @brief Reset the delay value + */ + void + reset(); + +protected: + /** + * @brief Compute the next delay value + * + * @return std::chrono::steady_clock::duration + */ + virtual std::chrono::steady_clock::duration + nextDelay() const = 0; +}; +using RetryStrategyPtr = std::unique_ptr; + +/** + * @brief A retry mechanism + */ +class Retry { + RetryStrategyPtr strategy_; + boost::asio::steady_timer timer_; + size_t attemptNumber_ = 0; + +public: + Retry(RetryStrategyPtr strategy, boost::asio::strand strand); + ~Retry(); + + /** + * @brief Schedule a retry + * + * @tparam Fn The type of the callable to execute + * @param func The callable to execute + */ + template + void + retry(Fn&& func) + { + timer_.expires_after(strategy_->getDelay()); + strategy_->increaseDelay(); + timer_.async_wait([this, func = std::forward(func)](boost::system::error_code const& ec) { + if (ec == boost::asio::error::operation_aborted) { + return; + } + ++attemptNumber_; + func(); + }); + } + + /** + * @brief Cancel scheduled retry if any + */ + void + cancel(); + + /** + * @brief Get the current attempt number + * + * @return size_t + */ + size_t + attemptNumber() const; + + /** + * @brief Get the current delay value + * + * @return std::chrono::steady_clock::duration + */ + std::chrono::steady_clock::duration + delayValue() const; + + /** + * @brief Reset the delay value and attempt number + */ + void + reset(); +}; + +/** + * @brief Create a retry mechanism with exponential backoff strategy + * + * @param delay The initial delay value + * @param maxDelay The maximum delay value + * @param strand The strand to use for async operations + * @return Retry + */ +class ExponentialBackoffStrategy : public RetryStrategy { + std::chrono::steady_clock::duration maxDelay_; + +public: + ExponentialBackoffStrategy(std::chrono::steady_clock::duration delay, std::chrono::steady_clock::duration maxDelay); + +private: + std::chrono::steady_clock::duration + nextDelay() const override; +}; + +/** + * @brief Create a retry mechanism with exponential backoff strategy + * + * @param delay The initial delay value + * @param maxDelay The maximum delay value + * @param strand The strand to use for async operations + * @return Retry + */ +Retry +makeRetryExponentialBackoff( + std::chrono::steady_clock::duration delay, + std::chrono::steady_clock::duration maxDelay, + boost::asio::strand strand +); + +} // namespace util diff --git a/src/util/log/Logger.cpp b/src/util/log/Logger.cpp index b825c776..de14d256 100644 --- a/src/util/log/Logger.cpp +++ b/src/util/log/Logger.cpp @@ -28,7 +28,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -55,11 +57,14 @@ #include #include #include +#include +#include namespace util { Logger LogService::general_log_ = Logger{"General"}; Logger LogService::alert_log_ = Logger{"Alert"}; +boost::log::filter LogService::filter_{}; std::ostream& operator<<(std::ostream& stream, Severity sev) @@ -156,9 +161,7 @@ LogService::init(util::Config const& config) "Performance", }; - auto core = boost::log::core::get(); - auto min_severity = boost::log::expressions::channel_severity_filter(log_channel, log_severity); - + std::unordered_map min_severity; for (auto const& channel : channels) min_severity[channel] = defaultSeverity; min_severity["Alert"] = Severity::WRN; // Channel for alerts, always warning severity @@ -171,7 +174,19 @@ LogService::init(util::Config const& config) min_severity[name] = cfg.valueOr("log_level", defaultSeverity); } - core->set_filter(min_severity); + auto log_filter = [min_severity = std::move(min_severity), + defaultSeverity](boost::log::attribute_value_set const& attributes) -> bool { + auto const channel = attributes[log_channel]; + auto const severity = attributes[log_severity]; + if (!channel || !severity) + return false; + if (auto const it = min_severity.find(channel.get()); it != min_severity.end()) + return severity.get() >= it->second; + return severity.get() >= defaultSeverity; + }; + + filter_ = boost::log::filter{std::move(log_filter)}; + boost::log::core::get()->set_filter(filter_); LOG(LogService::info()) << "Default log level = " << defaultSeverity; } diff --git a/src/util/log/Logger.hpp b/src/util/log/Logger.hpp index 8e995187..7cc21474 100644 --- a/src/util/log/Logger.hpp +++ b/src/util/log/Logger.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -226,6 +227,7 @@ public: class LogService { static Logger general_log_; /*< Global logger for General channel */ static Logger alert_log_; /*< Global logger for Alerts channel */ + static boost::log::filter filter_; public: LogService() = delete; diff --git a/src/util/requests/RequestBuilder.cpp b/src/util/requests/RequestBuilder.cpp index aba9f1e0..c0acddb9 100644 --- a/src/util/requests/RequestBuilder.cpp +++ b/src/util/requests/RequestBuilder.cpp @@ -20,6 +20,7 @@ #include "util/requests/RequestBuilder.hpp" #include "util/Expected.hpp" +#include "util/log/Logger.hpp" #include "util/requests/Types.hpp" #include "util/requests/impl/StreamData.hpp" @@ -43,6 +44,7 @@ #include #include #include +#include #include namespace util::requests { @@ -61,7 +63,7 @@ RequestBuilder::RequestBuilder(std::string host, std::string port) : host_(std:: RequestBuilder& RequestBuilder::addHeader(HttpHeader const& header) { - request_.set(header.name, header.value); + std::visit([&](auto const& name) { request_.set(name, header.value); }, header.name); return *this; } @@ -95,11 +97,16 @@ RequestBuilder::setTarget(std::string_view target) return *this; } -RequestBuilder& -RequestBuilder::setSslEnabled(bool const enabled) +Expected +RequestBuilder::getSsl(boost::asio::yield_context yield) { - sslEnabled_ = enabled; - return *this; + return doSslRequest(yield, http::verb::get); +} + +Expected +RequestBuilder::getPlain(boost::asio::yield_context yield) +{ + return doPlainRequest(yield, http::verb::get); } Expected @@ -108,6 +115,18 @@ RequestBuilder::get(asio::yield_context yield) return doRequest(yield, http::verb::get); } +Expected +RequestBuilder::postSsl(boost::asio::yield_context yield) +{ + return doSslRequest(yield, http::verb::post); +} + +Expected +RequestBuilder::postPlain(boost::asio::yield_context yield) +{ + return doPlainRequest(yield, http::verb::post); +} + Expected RequestBuilder::post(asio::yield_context yield) { @@ -115,28 +134,41 @@ RequestBuilder::post(asio::yield_context yield) } Expected -RequestBuilder::doRequest(asio::yield_context yield, beast::http::verb method) +RequestBuilder::doSslRequest(asio::yield_context yield, beast::http::verb method) { - if (sslEnabled_) { - auto streamData = impl::SslTcpStreamData::create(yield); - if (not streamData.has_value()) - return Unexpected{std::move(streamData).error()}; + auto streamData = impl::SslTcpStreamData::create(yield); + if (not streamData.has_value()) + return Unexpected{std::move(streamData).error()}; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wold-style-cast" - if (!SSL_set_tlsext_host_name(streamData->stream.native_handle(), host_.c_str())) { + if (!SSL_set_tlsext_host_name(streamData->stream.native_handle(), host_.c_str())) { #pragma GCC diagnostic pop - beast::error_code errorCode; - errorCode.assign(static_cast(::ERR_get_error()), asio::error::get_ssl_category()); - return Unexpected{RequestError{"SSL setup failed", errorCode}}; - } - return doRequestImpl(std::move(streamData).value(), yield, method); + beast::error_code errorCode; + errorCode.assign(static_cast(::ERR_get_error()), asio::error::get_ssl_category()); + return Unexpected{RequestError{"SSL setup failed", errorCode}}; } + return doRequestImpl(std::move(streamData).value(), yield, method); +} +Expected +RequestBuilder::doPlainRequest(asio::yield_context yield, beast::http::verb method) +{ auto streamData = impl::TcpStreamData{yield}; return doRequestImpl(std::move(streamData), yield, method); } +Expected +RequestBuilder::doRequest(asio::yield_context yield, beast::http::verb method) +{ + auto result = doSslRequest(yield, method); + if (result.has_value()) + return result; + + LOG(log_.debug()) << "SSL request failed: " << result.error().message() << ". Falling back to plain request."; + return doPlainRequest(yield, method); +} + template Expected RequestBuilder::doRequestImpl(StreamDataType&& streamData, asio::yield_context yield, http::verb const method) @@ -178,7 +210,7 @@ RequestBuilder::doRequestImpl(StreamDataType&& streamData, asio::yield_context y return Unexpected{RequestError{"Read error", errorCode}}; if (response.result() != http::status::ok) - return Unexpected{RequestError{"Response status not OK"}}; + return Unexpected{RequestError{"Response status is not OK"}}; beast::get_lowest_layer(stream).socket().shutdown(tcp::socket::shutdown_both, errorCode); diff --git a/src/util/requests/RequestBuilder.hpp b/src/util/requests/RequestBuilder.hpp index 838cb3f4..118d845f 100644 --- a/src/util/requests/RequestBuilder.hpp +++ b/src/util/requests/RequestBuilder.hpp @@ -20,6 +20,7 @@ #pragma once #include "util/Expected.hpp" +#include "util/log/Logger.hpp" #include "util/requests/Types.hpp" #include @@ -42,11 +43,11 @@ namespace util::requests { * @brief Builder for HTTP requests */ class RequestBuilder { + util::Logger log_{"RequestBuilder"}; std::string host_; std::string port_; std::chrono::milliseconds timeout_{DEFAULT_TIMEOUT}; boost::beast::http::request request_; - bool sslEnabled_{false}; public: /** @@ -107,18 +108,32 @@ public: setTarget(std::string_view target); /** - * @brief Set SSL enabled or disabled + * @brief Perform a GET request with SSL asynchronously * - * @note Default is false + * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is + * fine to call only get() or only post() of the same RequestBuilder from multiple threads. * - * @param ssl boolean value to set - * @return reference to itself + * @param yield yield context + * @return expected response or error */ - RequestBuilder& - setSslEnabled(bool enabled); + Expected + getSsl(boost::asio::yield_context yield); /** - * @brief Perform a GET request asynchronously + * @brief Perform a GET request without SSL asynchronously + * + * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is + * fine to call only get() or only post() of the same RequestBuilder from multiple threads. + * + * @param yield yield context + * @return expected response or error + */ + Expected + getPlain(boost::asio::yield_context yield); + + /** + * @brief Perform a GET request asynchronously. The SSL will be used first, if it fails, the plain connection will + * be used. * * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is * fine to call only get() or only post() of the same RequestBuilder from multiple threads. @@ -130,7 +145,32 @@ public: get(boost::asio::yield_context yield); /** - * @brief Perform a POST request asynchronously + * @brief Perform a POST request with SSL asynchronously + * + * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is + * fine to call only get() or only post() of the same RequestBuilder from multiple threads. + * + * @param yield yield context + * @return expected response or error + */ + Expected + postSsl(boost::asio::yield_context yield); + + /** + * @brief Perform a POST request without SSL asynchronously + * + * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is + * fine to call only get() or only post() of the same RequestBuilder from multiple threads. + * + * @param yield yield context + * @return expected response or error + */ + Expected + postPlain(boost::asio::yield_context yield); + + /** + * @brief Perform a POST request asynchronously. The SSL will be used first, if it fails, the plain connection will + * be used. * * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is * fine to call only get() or only post() of the same RequestBuilder from multiple threads. @@ -144,6 +184,12 @@ public: static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT{30000}; private: + Expected + doSslRequest(boost::asio::yield_context yield, boost::beast::http::verb method); + + Expected + doPlainRequest(boost::asio::yield_context yield, boost::beast::http::verb method); + Expected doRequest(boost::asio::yield_context yield, boost::beast::http::verb method); diff --git a/src/util/requests/Types.cpp b/src/util/requests/Types.cpp index 4c0037de..726f317f 100644 --- a/src/util/requests/Types.cpp +++ b/src/util/requests/Types.cpp @@ -19,26 +19,50 @@ #include "util/requests/Types.hpp" +#include "util/requests/impl/SslContext.hpp" + #include #include +#include #include #include namespace util::requests { -RequestError::RequestError(std::string message) : message(std::move(message)) +RequestError::RequestError(std::string message) : message_(std::move(message)) { } -RequestError::RequestError(std::string msg, boost::beast::error_code const& ec) : message(std::move(msg)) +RequestError::RequestError(std::string message, boost::beast::error_code errorCode) + : message_(std::move(message)), errorCode_(errorCode) { - message.append(": "); - message.append(ec.message()); + message_.append(": "); + if (auto const sslError = impl::sslErrorToString(errorCode); sslError.has_value()) { + message_.append(sslError.value()); + } else { + message_.append(errorCode.message()); + } +} + +std::string const& +RequestError::message() const +{ + return message_; +} + +std::optional const& +RequestError::errorCode() const +{ + return errorCode_; } HttpHeader::HttpHeader(boost::beast::http::field name, std::string value) : name(name), value(std::move(value)) { } +HttpHeader::HttpHeader(std::string name, std::string value) : name(std::move(name)), value(std::move(value)) +{ +} + } // namespace util::requests diff --git a/src/util/requests/Types.hpp b/src/util/requests/Types.hpp index c57ea384..35391741 100644 --- a/src/util/requests/Types.hpp +++ b/src/util/requests/Types.hpp @@ -22,14 +22,20 @@ #include #include +#include #include +#include namespace util::requests { /** * @brief Error type for HTTP requests */ -struct RequestError { +class RequestError { + std::string message_; + std::optional errorCode_; + +public: /** * @brief Construct a new Request Error object * @@ -43,9 +49,23 @@ struct RequestError { * @param message error message * @param ec error code from boost::beast */ - RequestError(std::string msg, boost::beast::error_code const& ec); + RequestError(std::string message, boost::beast::error_code errorCode); - std::string message; + /** + * @brief Get the error message + * + * @return std::string + */ + std::string const& + message() const; + + /** + * @brief Get the error code, if any + * + * @return std::optional + */ + std::optional const& + errorCode() const; }; /** @@ -53,8 +73,9 @@ struct RequestError { */ struct HttpHeader { HttpHeader(boost::beast::http::field name, std::string value); + HttpHeader(std::string name, std::string value); - boost::beast::http::field name; + std::variant name; std::string value; }; diff --git a/src/util/requests/WsConnection.cpp b/src/util/requests/WsConnection.cpp index 3a97fc7a..ddf8aa64 100644 --- a/src/util/requests/WsConnection.cpp +++ b/src/util/requests/WsConnection.cpp @@ -20,6 +20,7 @@ #include "util/requests/WsConnection.hpp" #include "util/Expected.hpp" +#include "util/log/Logger.hpp" #include "util/requests/Types.hpp" #include "util/requests/impl/StreamData.hpp" #include "util/requests/impl/WsConnectionImpl.hpp" @@ -78,39 +79,53 @@ WsConnectionBuilder::setTarget(std::string target) } WsConnectionBuilder& -WsConnectionBuilder::setConnectionTimeout(std::chrono::milliseconds timeout) +WsConnectionBuilder::setConnectionTimeout(std::chrono::steady_clock::duration timeout) { - timeout_ = timeout; + connectionTimeout_ = timeout; return *this; } WsConnectionBuilder& -WsConnectionBuilder::setSslEnabled(bool sslEnabled) +WsConnectionBuilder::setWsHandshakeTimeout(std::chrono::steady_clock::duration timeout) { - sslEnabled_ = sslEnabled; + wsHandshakeTimeout_ = timeout; return *this; } +Expected +WsConnectionBuilder::sslConnect(asio::yield_context yield) const +{ + auto streamData = impl::SslWsStreamData::create(yield); + if (not streamData.has_value()) + return Unexpected{std::move(streamData).error()}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + if (!SSL_set_tlsext_host_name(streamData->stream.next_layer().native_handle(), host_.c_str())) { +#pragma GCC diagnostic pop + beast::error_code errorCode; + errorCode.assign(static_cast(::ERR_get_error()), beast::net::error::get_ssl_category()); + return Unexpected{RequestError{"SSL setup failed", errorCode}}; + } + return connectImpl(std::move(streamData).value(), yield); +} + +Expected +WsConnectionBuilder::plainConnect(asio::yield_context yield) const +{ + return connectImpl(impl::WsStreamData{yield}, yield); +} + Expected WsConnectionBuilder::connect(asio::yield_context yield) const { - if (sslEnabled_) { - auto streamData = impl::SslWsStreamData::create(yield); - if (not streamData.has_value()) - return Unexpected{std::move(streamData).error()}; + auto sslConnection = sslConnect(yield); + if (sslConnection.has_value()) + return sslConnection; + LOG(log_.debug()) << "SSL connection failed with error: " << sslConnection.error().message() + << ". Falling back to plain connection."; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wold-style-cast" - if (!SSL_set_tlsext_host_name(streamData->stream.next_layer().native_handle(), host_.c_str())) { -#pragma GCC diagnostic pop - beast::error_code errorCode; - errorCode.assign(static_cast(::ERR_get_error()), beast::net::error::get_ssl_category()); - return Unexpected{RequestError{"SSL setup failed", errorCode}}; - } - return connectImpl(std::move(streamData).value(), yield); - } - - return connectImpl(impl::WsStreamData{yield}, yield); + return plainConnect(yield); } template @@ -127,13 +142,13 @@ WsConnectionBuilder::connectImpl(StreamDataType&& streamData, asio::yield_contex auto& ws = streamData.stream; - beast::get_lowest_layer(ws).expires_after(timeout_); + beast::get_lowest_layer(ws).expires_after(connectionTimeout_); auto endpoint = beast::get_lowest_layer(ws).async_connect(results, yield[errorCode]); if (errorCode) return Unexpected{RequestError{"Connect error", errorCode}}; if constexpr (StreamDataType::sslEnabled) { - beast::get_lowest_layer(ws).expires_after(timeout_); + beast::get_lowest_layer(ws).expires_after(connectionTimeout_); ws.next_layer().async_handshake(asio::ssl::stream_base::client, yield[errorCode]); if (errorCode) return Unexpected{RequestError{"SSL handshake error", errorCode}}; @@ -142,10 +157,12 @@ WsConnectionBuilder::connectImpl(StreamDataType&& streamData, asio::yield_contex // Turn off the timeout on the tcp_stream, because the websocket stream has its own timeout system beast::get_lowest_layer(ws).expires_never(); - ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + auto wsTimeout = websocket::stream_base::timeout::suggested(beast::role_type::client); + wsTimeout.handshake_timeout = wsHandshakeTimeout_; + ws.set_option(wsTimeout); ws.set_option(websocket::stream_base::decorator([this](websocket::request_type& req) { for (auto const& header : headers_) - req.set(header.name, header.value); + std::visit([&](auto const& name) { req.set(name, header.value); }, header.name); })); std::string const host = fmt::format("{}:{}", host_, endpoint.port()); diff --git a/src/util/requests/WsConnection.hpp b/src/util/requests/WsConnection.hpp index 3532e6b9..bb6b9cf1 100644 --- a/src/util/requests/WsConnection.hpp +++ b/src/util/requests/WsConnection.hpp @@ -20,8 +20,12 @@ #pragma once #include "util/Expected.hpp" +#include "util/log/Logger.hpp" #include "util/requests/Types.hpp" +#include +#include +#include #include #include #include @@ -31,22 +35,49 @@ #include #include #include +#include #include namespace util::requests { +/** + * @brief Interface for WebSocket connections. It is used to hide SSL and plain connections behind the same interface. + * + * @note WsConnection must not be destroyed while there are pending asynchronous operations on it. + */ class WsConnection { public: virtual ~WsConnection() = default; + /** + * @brief Read a message from the WebSocket + * + * @param yield yield context + * @return Expected message or error + */ virtual Expected read(boost::asio::yield_context yield) = 0; + /** + * @brief Write a message to the WebSocket + * + * @param message message to write + * @param yield yield context + * @return std::optional error if any + */ virtual std::optional write(std::string const& message, boost::asio::yield_context yield) = 0; + /** + * @brief Close the WebSocket + * + * @param yield yield context + * @return std::optional error if any + */ virtual std::optional - close(boost::asio::yield_context yield) = 0; + close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0; + + static constexpr std::chrono::seconds DEFAULT_TIMEOUT{5}; }; using WsConnectionPtr = std::unique_ptr; @@ -54,12 +85,13 @@ using WsConnectionPtr = std::unique_ptr; * @brief Builder for WebSocket connections */ class WsConnectionBuilder { + util::Logger log_{"WsConnectionBuilder"}; std::string host_; std::string port_; std::vector headers_; - std::chrono::milliseconds timeout_{DEFAULT_TIMEOUT}; + std::chrono::steady_clock::duration connectionTimeout_{DEFAULT_TIMEOUT}; + std::chrono::steady_clock::duration wsHandshakeTimeout_{DEFAULT_TIMEOUT}; std::string target_{"/"}; - bool sslEnabled_{false}; public: WsConnectionBuilder(std::string host, std::string port); @@ -92,27 +124,43 @@ public: setTarget(std::string target); /** - * @brief Set the timeout for connection establishing operations + * @brief Set the timeout for connection establishing operations. Default is 5 seconds * * @param timeout timeout to set * @return RequestBuilder& this */ WsConnectionBuilder& - setConnectionTimeout(std::chrono::milliseconds timeout); + setConnectionTimeout(std::chrono::steady_clock::duration timeout); /** - * @brief Set whether SSL is enabled + * @brief Set the timeout for WebSocket handshake. Default is 5 seconds * - * @note Default is false - * - * @param enabled whether SSL is enabled + * @param timeout timeout to set * @return RequestBuilder& this */ WsConnectionBuilder& - setSslEnabled(bool enabled); + setWsHandshakeTimeout(std::chrono::steady_clock::duration timeout); /** - * @brief Connect to the host asynchronously + * @brief Connect to the host using SSL asynchronously + * + * @param yield yield context + * @return Expected WebSocket connection or error + */ + Expected + sslConnect(boost::asio::yield_context yield) const; + + /** + * @brief Connect to the host without SSL asynchronously + * + * @param yield yield context + * @return Expected WebSocket connection or error + */ + Expected + plainConnect(boost::asio::yield_context yield) const; + + /** + * @brief Connect to the host trying SSL first then plain if SSL fails * * @param yield yield context * @return Expected WebSocket connection or error @@ -120,7 +168,7 @@ public: Expected connect(boost::asio::yield_context yield) const; - static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT{5000}; + static constexpr std::chrono::seconds DEFAULT_TIMEOUT{5}; private: template diff --git a/src/util/requests/impl/SslContext.cpp b/src/util/requests/impl/SslContext.cpp index 70ef8751..bcfb9637 100644 --- a/src/util/requests/impl/SslContext.cpp +++ b/src/util/requests/impl/SslContext.cpp @@ -20,16 +20,24 @@ #include "util/requests/impl/SslContext.hpp" #include "util/Expected.hpp" +#include "util/log/Logger.hpp" #include "util/requests/Types.hpp" #include #include +#include #include +#include +#include +#include +#include #include +#include #include #include #include +#include #include #include #include @@ -87,4 +95,24 @@ makeSslContext() return context; } +std::optional +sslErrorToString(boost::beast::error_code const& error) +{ + if (error.category() != boost::asio::error::get_ssl_category()) + return std::nullopt; + + std::string errorString = fmt::format( + "({},{}) ", + boost::lexical_cast(ERR_GET_LIB(error.value())), + boost::lexical_cast(ERR_GET_REASON(error.value())) + ); + + static constexpr size_t BUFFER_SIZE = 128; + char buf[BUFFER_SIZE]; + ::ERR_error_string_n(error.value(), buf, sizeof(buf)); + errorString += buf; + + return errorString; +} + } // namespace util::requests::impl diff --git a/src/util/requests/impl/SslContext.hpp b/src/util/requests/impl/SslContext.hpp index b639ee1c..d87aa40f 100644 --- a/src/util/requests/impl/SslContext.hpp +++ b/src/util/requests/impl/SslContext.hpp @@ -23,10 +23,17 @@ #include "util/requests/Types.hpp" #include +#include + +#include +#include namespace util::requests::impl { Expected makeSslContext(); +std::optional +sslErrorToString(boost::beast::error_code const& error); + } // namespace util::requests::impl diff --git a/src/util/requests/impl/WsConnectionImpl.hpp b/src/util/requests/impl/WsConnectionImpl.hpp index 99ed4341..6291097e 100644 --- a/src/util/requests/impl/WsConnectionImpl.hpp +++ b/src/util/requests/impl/WsConnectionImpl.hpp @@ -32,7 +32,9 @@ #include #include #include +#include +#include #include #include #include @@ -75,8 +77,15 @@ public: } std::optional - close(boost::asio::yield_context yield) override + close(boost::asio::yield_context yield, std::chrono::steady_clock::duration const timeout = DEFAULT_TIMEOUT) + override { + // Set the timeout for closing the connection + boost::beast::websocket::stream_base::timeout wsTimeout{}; + ws_.get_option(wsTimeout); + wsTimeout.handshake_timeout = timeout; + ws_.set_option(wsTimeout); + boost::beast::error_code errorCode; ws_.async_close(boost::beast::websocket::close_code::normal, yield[errorCode]); if (errorCode) diff --git a/unittests/data/cassandra/RetryPolicyTests.cpp b/unittests/data/cassandra/RetryPolicyTests.cpp index 2508c54e..b79dfc59 100644 --- a/unittests/data/cassandra/RetryPolicyTests.cpp +++ b/unittests/data/cassandra/RetryPolicyTests.cpp @@ -23,20 +23,19 @@ #include #include +#include #include -#include -#include - using namespace data::cassandra; using namespace data::cassandra::impl; using namespace testing; -class BackendCassandraRetryPolicyTest : public SyncAsioContextTest {}; +struct BackendCassandraRetryPolicyTest : SyncAsioContextTest { + ExponentialBackoffRetryPolicy retryPolicy{ctx}; +}; TEST_F(BackendCassandraRetryPolicyTest, ShouldRetryAlwaysTrue) { - auto retryPolicy = ExponentialBackoffRetryPolicy{ctx}; EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT})); EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"invalid data", CASS_ERROR_LIB_INVALID_DATA})); EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"invalid query", CASS_ERROR_SERVER_INVALID_QUERY})); @@ -48,37 +47,30 @@ TEST_F(BackendCassandraRetryPolicyTest, ShouldRetryAlwaysTrue) } } -TEST_F(BackendCassandraRetryPolicyTest, CheckComputedBackoffDelayIsCorrect) -{ - auto retryPolicy = ExponentialBackoffRetryPolicy{ctx}; - EXPECT_EQ(retryPolicy.calculateDelay(0).count(), 1); - EXPECT_EQ(retryPolicy.calculateDelay(1).count(), 2); - EXPECT_EQ(retryPolicy.calculateDelay(2).count(), 4); - EXPECT_EQ(retryPolicy.calculateDelay(3).count(), 8); - EXPECT_EQ(retryPolicy.calculateDelay(4).count(), 16); - EXPECT_EQ(retryPolicy.calculateDelay(5).count(), 32); - EXPECT_EQ(retryPolicy.calculateDelay(6).count(), 64); - EXPECT_EQ(retryPolicy.calculateDelay(7).count(), 128); - EXPECT_EQ(retryPolicy.calculateDelay(8).count(), 256); - EXPECT_EQ(retryPolicy.calculateDelay(9).count(), 512); - EXPECT_EQ(retryPolicy.calculateDelay(10).count(), 1024); - EXPECT_EQ(retryPolicy.calculateDelay(11).count(), - 1024); // 10 is max, same after that -} - TEST_F(BackendCassandraRetryPolicyTest, RetryCorrectlyExecuted) { - auto callCount = std::atomic_int{0}; - auto work = std::optional{ctx}; - auto retryPolicy = ExponentialBackoffRetryPolicy{ctx}; + StrictMock> callback; + EXPECT_CALL(callback, Call()).Times(3); - retryPolicy.retry([&callCount]() { ++callCount; }); - retryPolicy.retry([&callCount]() { ++callCount; }); - retryPolicy.retry([&callCount, &work]() { - ++callCount; - work.reset(); - }); - - ctx.run(); - ASSERT_EQ(callCount, 3); + for (auto i = 0; i < 3; ++i) { + retryPolicy.retry([&callback]() { callback.Call(); }); + runContext(); + } +} + +TEST_F(BackendCassandraRetryPolicyTest, MutlipleRetryCancelPreviousCalls) +{ + StrictMock> callback; + EXPECT_CALL(callback, Call()); + + for (auto i = 0; i < 3; ++i) + retryPolicy.retry([&callback]() { callback.Call(); }); + + runContext(); +} + +TEST_F(BackendCassandraRetryPolicyTest, CallbackIsNotCalledIfContextDies) +{ + StrictMock> callback; + retryPolicy.retry([&callback]() { callback.Call(); }); } diff --git a/unittests/etl/ExtractorTests.cpp b/unittests/etl/ExtractorTests.cpp index d8da8104..3fefded8 100644 --- a/unittests/etl/ExtractorTests.cpp +++ b/unittests/etl/ExtractorTests.cpp @@ -52,7 +52,6 @@ public: void SetUp() override { - NoLoggerFixture::SetUp(); state_.isStopping = false; state_.writeConflict = false; state_.isReadOnly = false; @@ -63,7 +62,6 @@ public: TearDown() override { extractor_.reset(); - NoLoggerFixture::TearDown(); } }; diff --git a/unittests/etl/ForwardingSourceTests.cpp b/unittests/etl/ForwardingSourceTests.cpp new file mode 100644 index 00000000..e7e819a4 --- /dev/null +++ b/unittests/etl/ForwardingSourceTests.cpp @@ -0,0 +1,120 @@ +//------------------------------------------------------------------------------ +/* + 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/ForwardingSource.hpp" +#include "util/Fixtures.hpp" +#include "util/TestWsServer.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace etl::impl; + +struct ForwardingSourceTests : SyncAsioContextTest { + TestWsServer server_{ctx, "0.0.0.0", 11114}; + ForwardingSource forwardingSource{"127.0.0.1", "11114", std::chrono::milliseconds{1}}; +}; + +TEST_F(ForwardingSourceTests, ConnectionFailed) +{ + runSpawn([&](boost::asio::yield_context yield) { + auto result = forwardingSource.forwardToRippled({}, {}, yield); + EXPECT_FALSE(result); + }); +} + +struct ForwardingSourceOperationsTests : ForwardingSourceTests { + std::string const message_ = R"({"data":"some_data"})"; + boost::json::object const reply_ = {{"reply", "some_reply"}}; + + TestWsConnection + serverConnection(boost::asio::yield_context yield) + { + // First connection attempt is SSL handshake so it will fail + auto failedConnection = server_.acceptConnection(yield); + [&]() { ASSERT_FALSE(failedConnection); }(); + + auto connection = server_.acceptConnection(yield); + [&]() { ASSERT_TRUE(connection); }(); + return std::move(connection).value(); + } +}; + +TEST_F(ForwardingSourceOperationsTests, ReadFailed) +{ + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto connection = serverConnection(yield); + connection.close(yield); + }); + + runSpawn([&](boost::asio::yield_context yield) { + auto result = forwardingSource.forwardToRippled(boost::json::parse(message_).as_object(), {}, yield); + EXPECT_FALSE(result); + }); +} + +TEST_F(ForwardingSourceOperationsTests, ParseFailed) +{ + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto connection = serverConnection(yield); + + auto receivedMessage = connection.receive(yield); + [&]() { ASSERT_TRUE(receivedMessage); }(); + EXPECT_EQ(*receivedMessage, message_); + + auto sendError = connection.send("invalid_json", yield); + [&]() { ASSERT_FALSE(sendError) << *sendError; }(); + + connection.close(yield); + }); + + runSpawn([&](boost::asio::yield_context yield) { + auto result = forwardingSource.forwardToRippled(boost::json::parse(message_).as_object(), {}, yield); + EXPECT_FALSE(result); + }); +} + +TEST_F(ForwardingSourceOperationsTests, Success) +{ + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto connection = serverConnection(yield); + + auto receivedMessage = connection.receive(yield); + [&]() { ASSERT_TRUE(receivedMessage); }(); + EXPECT_EQ(*receivedMessage, message_); + + auto sendError = connection.send(boost::json::serialize(reply_), yield); + [&]() { ASSERT_FALSE(sendError) << *sendError; }(); + }); + + runSpawn([&](boost::asio::yield_context yield) { + auto result = forwardingSource.forwardToRippled(boost::json::parse(message_).as_object(), "some_ip", yield); + [&]() { ASSERT_TRUE(result); }(); + auto expectedReply = reply_; + expectedReply["forwarded"] = true; + EXPECT_EQ(*result, expectedReply) << *result; + }); +} diff --git a/unittests/etl/GrpcSourceTests.cpp b/unittests/etl/GrpcSourceTests.cpp new file mode 100644 index 00000000..0be696b5 --- /dev/null +++ b/unittests/etl/GrpcSourceTests.cpp @@ -0,0 +1,150 @@ +//------------------------------------------------------------------------------ +/* + 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/GrpcSource.hpp" +#include "util/Fixtures.hpp" +#include "util/MockBackend.hpp" +#include "util/MockXrpLedgerAPIService.hpp" +#include "util/TestObject.hpp" +#include "util/config/Config.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace etl::impl; + +struct GrpcSourceTests : NoLoggerFixture, unittests::util::WithMockXrpLedgerAPIService { + GrpcSourceTests() + : WithMockXrpLedgerAPIService("localhost:50051") + , mockBackend_(std::make_shared>(util::Config{})) + , grpcSource_("127.0.0.1", "50051", mockBackend_) + { + } + + std::shared_ptr> mockBackend_; + testing::StrictMock grpcSource_; +}; + +TEST_F(GrpcSourceTests, fetchLedger) +{ + uint32_t const sequence = 123; + bool const getObjects = true; + bool const getObjectNeighbors = false; + + EXPECT_CALL(mockXrpLedgerAPIService, GetLedger) + .WillOnce([&](grpc::ServerContext* /*context*/, + org::xrpl::rpc::v1::GetLedgerRequest const* request, + org::xrpl::rpc::v1::GetLedgerResponse* response) { + EXPECT_EQ(request->ledger().sequence(), sequence); + EXPECT_TRUE(request->transactions()); + EXPECT_TRUE(request->expand()); + EXPECT_EQ(request->get_objects(), getObjects); + EXPECT_EQ(request->get_object_neighbors(), getObjectNeighbors); + EXPECT_EQ(request->user(), "ETL"); + response->set_validated(true); + response->set_is_unlimited(false); + response->set_object_neighbors_included(false); + return grpc::Status{}; + }); + auto const [status, response] = grpcSource_.fetchLedger(sequence, getObjects, getObjectNeighbors); + ASSERT_TRUE(status.ok()); + EXPECT_TRUE(response.validated()); + EXPECT_FALSE(response.is_unlimited()); + EXPECT_FALSE(response.object_neighbors_included()); +} + +TEST_F(GrpcSourceTests, fetchLedgerNoStub) +{ + testing::StrictMock wrongGrpcSource{"wrong", "wrong", mockBackend_}; + auto const [status, _response] = wrongGrpcSource.fetchLedger(0, false, false); + EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL); +} + +TEST_F(GrpcSourceTests, loadInitialLedgerNoStub) +{ + testing::StrictMock wrongGrpcSource{"wrong", "wrong", mockBackend_}; + auto const [data, success] = wrongGrpcSource.loadInitialLedger(0, 0, false); + EXPECT_TRUE(data.empty()); + EXPECT_FALSE(success); +} + +struct GrpcSourceLoadInitialLedgerTests : GrpcSourceTests { + uint32_t const sequence_ = 123; + uint32_t const numMarkers_ = 4; + bool const cacheOnly_ = false; +}; + +TEST_F(GrpcSourceLoadInitialLedgerTests, GetLedgerDataFailed) +{ + EXPECT_CALL(mockXrpLedgerAPIService, GetLedgerData) + .Times(numMarkers_) + .WillRepeatedly([&](grpc::ServerContext* /*context*/, + org::xrpl::rpc::v1::GetLedgerDataRequest const* request, + org::xrpl::rpc::v1::GetLedgerDataResponse* /*response*/) { + EXPECT_EQ(request->ledger().sequence(), sequence_); + EXPECT_EQ(request->user(), "ETL"); + return grpc::Status{grpc::StatusCode::NOT_FOUND, "Not found"}; + }); + + auto const [data, success] = grpcSource_.loadInitialLedger(sequence_, numMarkers_, cacheOnly_); + EXPECT_TRUE(data.empty()); + EXPECT_FALSE(success); +} + +TEST_F(GrpcSourceLoadInitialLedgerTests, worksFine) +{ + auto const key = ripple::uint256{4}; + std::string const keyStr{reinterpret_cast(key.data()), ripple::uint256::size()}; + auto const object = CreateTicketLedgerObject("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", sequence_); + auto const objectData = object.getSerializer().peekData(); + + EXPECT_CALL(mockXrpLedgerAPIService, GetLedgerData) + .Times(numMarkers_) + .WillRepeatedly([&](grpc::ServerContext* /*context*/, + org::xrpl::rpc::v1::GetLedgerDataRequest const* request, + org::xrpl::rpc::v1::GetLedgerDataResponse* response) { + EXPECT_EQ(request->ledger().sequence(), sequence_); + EXPECT_EQ(request->user(), "ETL"); + + response->set_is_unlimited(true); + auto newObject = response->mutable_ledger_objects()->add_objects(); + newObject->set_key(key.data(), ripple::uint256::size()); + newObject->set_data(objectData.data(), objectData.size()); + + return grpc::Status{}; + }); + + EXPECT_CALL(*mockBackend_, writeNFTs).Times(numMarkers_); + EXPECT_CALL(*mockBackend_, writeLedgerObject).Times(numMarkers_); + + auto const [data, success] = grpcSource_.loadInitialLedger(sequence_, numMarkers_, cacheOnly_); + + EXPECT_TRUE(success); + EXPECT_EQ(data, std::vector(4, keyStr)); +} diff --git a/unittests/etl/SourceTests.cpp b/unittests/etl/SourceTests.cpp new file mode 100644 index 00000000..bf9bc379 --- /dev/null +++ b/unittests/etl/SourceTests.cpp @@ -0,0 +1,186 @@ +//------------------------------------------------------------------------------ +/* + 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/Source.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace etl; + +using testing::Return; +using testing::StrictMock; + +struct GrpcSourceMock { + using FetchLedgerReturnType = std::pair; + MOCK_METHOD(FetchLedgerReturnType, fetchLedger, (uint32_t, bool, bool)); + + using LoadLedgerReturnType = std::pair, bool>; + MOCK_METHOD(LoadLedgerReturnType, loadInitialLedger, (uint32_t, uint32_t, bool)); +}; + +struct SubscriptionSourceMock { + MOCK_METHOD(void, run, ()); + MOCK_METHOD(bool, hasLedger, (uint32_t), (const)); + MOCK_METHOD(bool, isConnected, (), (const)); + MOCK_METHOD(void, setForwarding, (bool)); + MOCK_METHOD(std::chrono::steady_clock::time_point, lastMessageTime, (), (const)); + MOCK_METHOD(std::string, validatedRange, (), (const)); + MOCK_METHOD(void, stop, ()); +}; + +struct ForwardingSourceMock { + MOCK_METHOD(void, constructor, (std::string const&, std::string const&, std::chrono::steady_clock::duration)); + + using ForwardToRippledReturnType = std::optional; + using ClientIpOpt = std::optional; + MOCK_METHOD( + ForwardToRippledReturnType, + forwardToRippled, + (boost::json::object const&, ClientIpOpt const&, boost::asio::yield_context) + ); +}; + +struct SourceTest : public ::testing::Test { + boost::asio::io_context ioc_; + + StrictMock grpcSourceMock_; + std::shared_ptr> subscriptionSourceMock_ = + std::make_shared>(); + StrictMock forwardingSourceMock_; + + SourceImpl< + StrictMock&, + std::shared_ptr>, + StrictMock&> + source_{ + "some_ip", + "some_ws_port", + "some_grpc_port", + grpcSourceMock_, + subscriptionSourceMock_, + forwardingSourceMock_ + }; +}; + +TEST_F(SourceTest, run) +{ + EXPECT_CALL(*subscriptionSourceMock_, run()); + source_.run(); +} + +TEST_F(SourceTest, isConnected) +{ + EXPECT_CALL(*subscriptionSourceMock_, isConnected()).WillOnce(testing::Return(true)); + EXPECT_TRUE(source_.isConnected()); +} + +TEST_F(SourceTest, setForwarding) +{ + EXPECT_CALL(*subscriptionSourceMock_, setForwarding(true)); + source_.setForwarding(true); +} + +TEST_F(SourceTest, toJson) +{ + EXPECT_CALL(*subscriptionSourceMock_, validatedRange()).WillOnce(Return(std::string("some_validated_range"))); + EXPECT_CALL(*subscriptionSourceMock_, isConnected()).WillOnce(Return(true)); + auto const lastMessageTime = std::chrono::steady_clock::now(); + EXPECT_CALL(*subscriptionSourceMock_, lastMessageTime()).WillOnce(Return(lastMessageTime)); + + auto const json = source_.toJson(); + + EXPECT_EQ(boost::json::value_to(json.at("validated_range")), "some_validated_range"); + EXPECT_EQ(boost::json::value_to(json.at("is_connected")), "1"); + EXPECT_EQ(boost::json::value_to(json.at("ip")), "some_ip"); + EXPECT_EQ(boost::json::value_to(json.at("ws_port")), "some_ws_port"); + EXPECT_EQ(boost::json::value_to(json.at("grpc_port")), "some_grpc_port"); + auto lastMessageAgeStr = boost::json::value_to(json.at("last_msg_age_seconds")); + EXPECT_GE(std::stoi(lastMessageAgeStr), 0); +} + +TEST_F(SourceTest, toString) +{ + EXPECT_CALL(*subscriptionSourceMock_, validatedRange()).WillOnce(Return(std::string("some_validated_range"))); + + auto const str = source_.toString(); + EXPECT_EQ( + str, + "{validated range: some_validated_range, ip: some_ip, web socket port: some_ws_port, grpc port: some_grpc_port}" + ); +} + +TEST_F(SourceTest, hasLedger) +{ + uint32_t const ledgerSeq = 123; + EXPECT_CALL(*subscriptionSourceMock_, hasLedger(ledgerSeq)).WillOnce(Return(true)); + EXPECT_TRUE(source_.hasLedger(ledgerSeq)); +} + +TEST_F(SourceTest, fetchLedger) +{ + uint32_t const ledgerSeq = 123; + + EXPECT_CALL(grpcSourceMock_, fetchLedger(ledgerSeq, true, false)); + auto const [actualStatus, actualResponse] = source_.fetchLedger(ledgerSeq); + + EXPECT_EQ(actualStatus.error_code(), grpc::StatusCode::OK); +} + +TEST_F(SourceTest, loadInitialLedger) +{ + uint32_t const ledgerSeq = 123; + uint32_t const numMarkers = 3; + + EXPECT_CALL(grpcSourceMock_, loadInitialLedger(ledgerSeq, numMarkers, false)) + .WillOnce(Return(std::make_pair(std::vector{}, true))); + auto const [actualLedgers, actualSuccess] = source_.loadInitialLedger(ledgerSeq, numMarkers); + + EXPECT_TRUE(actualLedgers.empty()); + EXPECT_TRUE(actualSuccess); +} + +TEST_F(SourceTest, forwardToRippled) +{ + boost::json::object const request = {{"some_key", "some_value"}}; + std::optional const clientIp = "some_client_ip"; + + EXPECT_CALL(forwardingSourceMock_, forwardToRippled(request, clientIp, testing::_)).WillOnce(Return(request)); + + boost::asio::io_context ioContext; + boost::asio::spawn(ioContext, [&](boost::asio::yield_context yield) { + auto const response = source_.forwardToRippled(request, clientIp, yield); + EXPECT_EQ(response, request); + }); + ioContext.run(); +} diff --git a/unittests/etl/SubscriptionSourceDependenciesTests.cpp b/unittests/etl/SubscriptionSourceDependenciesTests.cpp new file mode 100644 index 00000000..f79aba75 --- /dev/null +++ b/unittests/etl/SubscriptionSourceDependenciesTests.cpp @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +#include +#include + +using namespace etl::impl; +using testing::StrictMock; + +struct SubscriptionSourceDependenciesTest : testing::Test { + std::shared_ptr> networkValidatedLedgers_ = + std::make_shared>(); + + std::shared_ptr> subscriptionManager_ = + std::make_shared>(); + + 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); +} diff --git a/unittests/etl/SubscriptionSourceTests.cpp b/unittests/etl/SubscriptionSourceTests.cpp new file mode 100644 index 00000000..93e13758 --- /dev/null +++ b/unittests/etl/SubscriptionSourceTests.cpp @@ -0,0 +1,493 @@ +//------------------------------------------------------------------------------ +/* + 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/SubscriptionSource.hpp" +#include "util/Fixtures.hpp" +#include "util/MockNetworkValidatedLedgers.hpp" +#include "util/MockSubscriptionManager.hpp" +#include "util/TestWsServer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace etl::impl; +using testing::MockFunction; +using testing::StrictMock; + +struct SubscriptionSourceConnectionTests : public NoLoggerFixture { + SubscriptionSourceConnectionTests() + { + subscriptionSource_->run(); + } + + boost::asio::io_context ioContext_; + + TestWsServer wsServer_{ioContext_, "0.0.0.0", 11113}; + + template + using StrictMockPtr = std::shared_ptr>; + + StrictMockPtr networkValidatedLedgers_ = + std::make_shared>(); + StrictMockPtr subscriptionManager_ = + std::make_shared>(); + + StrictMock> onConnectHook_; + StrictMock> onDisconnectHook_; + + std::unique_ptr subscriptionSource_ = std::make_unique( + ioContext_, + "127.0.0.1", + "11113", + networkValidatedLedgers_, + subscriptionManager_, + onConnectHook_.AsStdFunction(), + onDisconnectHook_.AsStdFunction(), + std::chrono::milliseconds(1), + std::chrono::milliseconds(1) + ); + + [[maybe_unused]] TestWsConnection + serverConnection(boost::asio::yield_context yield) + { + // The first one is an SSL attempt + auto failedConnection = wsServer_.acceptConnection(yield); + [&]() { ASSERT_FALSE(failedConnection); }(); + + auto connection = wsServer_.acceptConnection(yield); + [&]() { ASSERT_TRUE(connection) << connection.error().message(); }(); + + auto message = connection->receive(yield); + [&]() { + ASSERT_TRUE(message); + EXPECT_EQ( + message.value(), + R"({"command":"subscribe","streams":["ledger","manifests","validations","transactions_proposed"]})" + ); + }(); + return std::move(connection).value(); + } +}; + +TEST_F(SubscriptionSourceConnectionTests, ConnectionFailed) +{ + 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(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceConnectionTests, ReadError) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = serverConnection(yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceConnectionTests, ReadError_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + for (int i = 0; i < 2; ++i) { + auto connection = serverConnection(yield); + connection.close(yield); + } + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceConnectionTests, 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(); }); + ioContext_.run(); +} + +struct SubscriptionSourceReadTests : public SubscriptionSourceConnectionTests { + [[maybe_unused]] TestWsConnection + connectAndSendMessage(std::string const message, boost::asio::yield_context yield) + { + auto connection = serverConnection(yield); + auto error = connection.send(message, yield); + [&]() { ASSERT_FALSE(error) << *error; }(); + return connection; + } +}; + +TEST_F(SubscriptionSourceReadTests, GotWrongMessage_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage("something", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotResult) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndex) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"ledger_index":123}})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + EXPECT_CALL(*networkValidatedLedgers_, push(123)); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAsString_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"ledger_index":"123"}})", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersAsNumber_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":123}})", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + 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)); + + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":"123-456,789,32"}})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + 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_EQ(subscriptionSource_->validatedRange(), "123-456,789,32"); +} + +TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersWrongValue_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":"123-456-789,32"}})", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + 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)); + + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"result":{"ledger_index":123,"validated_ledgers":"1-3"}})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + 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)); +} + +TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndex) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","ledger_index":123})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + EXPECT_CALL(*networkValidatedLedgers_, push(123)); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAsString_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","ledger_index":"123"}})", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GorLedgerClosedWithValidatedLedgersAsNumber_Reconnect) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","validated_ledgers":123})", yield); + // We have to schedule receiving to receive close frame and boost will handle it automatically + connection.receive(yield); + serverConnection(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()).Times(2); + 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)); + + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","validated_ledgers":"1-2"})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + 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"); +} + +TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAndValidatedLedgers) +{ + 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","ledger_index":123,"validated_ledgers":"1-2"})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + 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"); +} + +TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingFalse) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"transaction":"some_transaction_data"})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingTrue) +{ + subscriptionSource_->setForwarding(true); + boost::json::object const message = {{"transaction", "some_transaction_data"}}; + + boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(boost::json::serialize(message), yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + EXPECT_CALL(*subscriptionManager_, forwardProposedTransaction(message)); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingFalse) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"validationReceived"})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingTrue) +{ + subscriptionSource_->setForwarding(true); + boost::json::object const message = {{"type", "validationReceived"}}; + + boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(boost::json::serialize(message), yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + EXPECT_CALL(*subscriptionManager_, forwardValidation(message)); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotManiefstReceivedIsForwardingFalse) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(R"({"type":"manifestReceived"})", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, GotManifestReceivedIsForwardingTrue) +{ + subscriptionSource_->setForwarding(true); + boost::json::object const message = {{"type", "manifestReceived"}}; + + boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage(boost::json::serialize(message), yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + EXPECT_CALL(*subscriptionManager_, forwardManifest(message)); + ioContext_.run(); +} + +TEST_F(SubscriptionSourceReadTests, LastMessageTime) +{ + boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) { + auto connection = connectAndSendMessage("some_message", yield); + connection.close(yield); + }); + + EXPECT_CALL(onConnectHook_, Call()); + EXPECT_CALL(onDisconnectHook_, Call()).WillOnce([this]() { subscriptionSource_->stop(); }); + ioContext_.run(); + + auto const actualLastTimeMessage = subscriptionSource_->lastMessageTime(); + auto const now = std::chrono::steady_clock::now(); + auto const diff = std::chrono::duration_cast(now - actualLastTimeMessage); + EXPECT_LT(diff, std::chrono::milliseconds(100)); +} diff --git a/unittests/util/Fixtures.hpp b/unittests/util/Fixtures.hpp index 255ce822..318cf8b4 100644 --- a/unittests/util/Fixtures.hpp +++ b/unittests/util/Fixtures.hpp @@ -70,10 +70,9 @@ class LoggerFixture : virtual public ::testing::Test { FakeBuffer buffer_; std::ostream stream_ = std::ostream{&buffer_}; -protected: +public: // Simulates the `util::Logger::init(config)` call - void - SetUp() override + LoggerFixture() { static std::once_flag once_; std::call_once(once_, [] { @@ -94,6 +93,7 @@ protected: core->set_logging_enabled(true); } +protected: void checkEqual(std::string expected) { @@ -113,12 +113,9 @@ protected: * * This is meant to be used as a base for other fixtures. */ -class NoLoggerFixture : virtual public LoggerFixture { -protected: - void - SetUp() override +struct NoLoggerFixture : virtual LoggerFixture { + NoLoggerFixture() { - LoggerFixture::SetUp(); boost::log::core::get()->set_logging_enabled(false); } }; @@ -174,14 +171,20 @@ struct SyncAsioContextTest : virtual public NoLoggerFixture { { using namespace boost::asio; - auto called = false; + testing::MockFunction call; spawn(ctx, [&, _ = make_work_guard(ctx)](yield_context yield) { f(yield); - called = true; + call.Call(); }); + EXPECT_CALL(call, Call()); + runContext(); + } + + void + runContext() + { ctx.run(); - ASSERT_TRUE(called); ctx.reset(); } @@ -194,16 +197,9 @@ struct MockBackendTestBase : virtual public NoLoggerFixture { void SetUp() override { - NoLoggerFixture::SetUp(); backend.reset(); } - void - TearDown() override - { - NoLoggerFixture::TearDown(); - } - class BackendProxy { std::shared_ptr backend; @@ -276,7 +272,6 @@ struct MockSubscriptionManagerTest : virtual public NoLoggerFixture { void SetUp() override { - NoLoggerFixture::SetUp(); mockSubscriptionManagerPtr = std::make_shared(); } void @@ -296,7 +291,6 @@ struct MockLoadBalancerTest : virtual public NoLoggerFixture { void SetUp() override { - NoLoggerFixture::SetUp(); mockLoadBalancerPtr = std::make_shared(); } void @@ -316,7 +310,6 @@ struct MockETLServiceTest : virtual public NoLoggerFixture { void SetUp() override { - NoLoggerFixture::SetUp(); mockETLServicePtr = std::make_shared(); } void @@ -336,7 +329,6 @@ struct MockCountersTest : virtual public NoLoggerFixture { void SetUp() override { - NoLoggerFixture::SetUp(); mockCountersPtr = std::make_shared(); } void diff --git a/unittests/util/MockLoadBalancer.hpp b/unittests/util/MockLoadBalancer.hpp index 15de2f90..73ce7c1d 100644 --- a/unittests/util/MockLoadBalancer.hpp +++ b/unittests/util/MockLoadBalancer.hpp @@ -19,7 +19,6 @@ #pragma once -#include "etl/Source.hpp" #include "util/FakeFetchResponse.hpp" #include @@ -37,7 +36,6 @@ struct MockLoadBalancer { MOCK_METHOD(void, loadInitialLedger, (std::uint32_t, bool), ()); MOCK_METHOD(std::optional, fetchLedger, (uint32_t, bool, bool), ()); - MOCK_METHOD(bool, shouldPropagateTxnStream, (etl::Source*), (const)); MOCK_METHOD(boost::json::value, toJson, (), (const)); MOCK_METHOD( std::optional, diff --git a/unittests/util/MockSource.hpp b/unittests/util/MockSource.hpp index 7abdae0d..a636f565 100644 --- a/unittests/util/MockSource.hpp +++ b/unittests/util/MockSource.hpp @@ -18,8 +18,6 @@ //============================================================================== #pragma once -#include "etl/Source.hpp" - #include #include #include @@ -33,33 +31,28 @@ #include #include -class MockSource : public etl::Source { +class MockSource { public: - MOCK_METHOD(bool, isConnected, (), (const, override)); - MOCK_METHOD(boost::json::object, toJson, (), (const, override)); - MOCK_METHOD(void, run, (), (override)); - MOCK_METHOD(void, pause, (), (override)); - MOCK_METHOD(void, resume, (), (override)); - MOCK_METHOD(std::string, toString, (), (const, override)); - MOCK_METHOD(bool, hasLedger, (uint32_t), (const, override)); - MOCK_METHOD( - (std::pair), - fetchLedger, - (uint32_t, bool, bool), - (override) - ); - MOCK_METHOD((std::pair, bool>), loadInitialLedger, (uint32_t, uint32_t, bool), (override)); + 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), fetchLedger, (uint32_t, bool, bool)); + MOCK_METHOD((std::pair, bool>), loadInitialLedger, (uint32_t, uint32_t, bool)); MOCK_METHOD( std::optional, forwardToRippled, (boost::json::object const&, std::optional const&, boost::asio::yield_context), - (const, override) + (const) ); MOCK_METHOD( std::optional, requestFromRippled, (boost::json::object const&, std::optional const&, boost::asio::yield_context), - (const, override) + (const) ); - MOCK_METHOD(boost::uuids::uuid, token, (), (const, override)); + MOCK_METHOD(boost::uuids::uuid, token, (), (const)); }; diff --git a/unittests/util/MockSubscriptionManager.hpp b/unittests/util/MockSubscriptionManager.hpp index d0aa32d8..e5817dca 100644 --- a/unittests/util/MockSubscriptionManager.hpp +++ b/unittests/util/MockSubscriptionManager.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include diff --git a/unittests/util/MockXrpLedgerAPIService.hpp b/unittests/util/MockXrpLedgerAPIService.hpp new file mode 100644 index 00000000..6b7c75a3 --- /dev/null +++ b/unittests/util/MockXrpLedgerAPIService.hpp @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace unittests::util { + +struct MockXrpLedgerAPIService final : public org::xrpl::rpc::v1::XRPLedgerAPIService::Service { + ~MockXrpLedgerAPIService() override = default; + + MOCK_METHOD( + grpc::Status, + GetLedger, + (grpc::ServerContext * context, + org::xrpl::rpc::v1::GetLedgerRequest const* request, + org::xrpl::rpc::v1::GetLedgerResponse* response), + (override) + ); + + MOCK_METHOD( + grpc::Status, + GetLedgerEntry, + (grpc::ServerContext * context, + org::xrpl::rpc::v1::GetLedgerEntryRequest const* request, + org::xrpl::rpc::v1::GetLedgerEntryResponse* response), + (override) + ); + + MOCK_METHOD( + grpc::Status, + GetLedgerData, + (grpc::ServerContext * context, + org::xrpl::rpc::v1::GetLedgerDataRequest const* request, + org::xrpl::rpc::v1::GetLedgerDataResponse* response), + (override) + ); + + MOCK_METHOD( + grpc::Status, + GetLedgerDiff, + (grpc::ServerContext * context, + org::xrpl::rpc::v1::GetLedgerDiffRequest const* request, + org::xrpl::rpc::v1::GetLedgerDiffResponse* response), + (override) + ); +}; + +struct WithMockXrpLedgerAPIService : virtual ::testing::Test { + WithMockXrpLedgerAPIService(std::string serverAddress) + { + grpc::ServerBuilder builder; + builder.AddListeningPort(serverAddress, grpc::InsecureServerCredentials()); + builder.RegisterService(&mockXrpLedgerAPIService); + server_ = builder.BuildAndStart(); + serverThread_ = std::thread([this] { server_->Wait(); }); + } + + ~WithMockXrpLedgerAPIService() override + { + server_->Shutdown(); + serverThread_.join(); + } + + MockXrpLedgerAPIService mockXrpLedgerAPIService; + +private: + std::unique_ptr server_; + std::thread serverThread_; +}; + +} // namespace unittests::util diff --git a/unittests/util/RetryTests.cpp b/unittests/util/RetryTests.cpp new file mode 100644 index 00000000..ada4315c --- /dev/null +++ b/unittests/util/RetryTests.cpp @@ -0,0 +1,107 @@ +//------------------------------------------------------------------------------ +/* + 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/Fixtures.hpp" +#include "util/Retry.hpp" + +#include +#include +#include + +#include +#include + +using namespace util; + +struct RetryTests : virtual ::testing::Test { + std::chrono::milliseconds const delay_{1}; + std::chrono::milliseconds const maxDelay_{5}; +}; + +TEST_F(RetryTests, ExponentialBackoffStrategy) +{ + ExponentialBackoffStrategy strategy{delay_, maxDelay_}; + + EXPECT_EQ(strategy.getDelay(), delay_); + + strategy.increaseDelay(); + EXPECT_EQ(strategy.getDelay(), delay_ * 2); + + strategy.increaseDelay(); + EXPECT_LT(strategy.getDelay(), maxDelay_); + + for (size_t i = 0; i < 10; ++i) { + strategy.increaseDelay(); + EXPECT_EQ(strategy.getDelay(), maxDelay_); + EXPECT_EQ(strategy.getDelay(), maxDelay_); + } + + strategy.reset(); + EXPECT_EQ(strategy.getDelay(), delay_); +} + +struct RetryWithExponentialBackoffStrategyTests : SyncAsioContextTest, RetryTests { + RetryWithExponentialBackoffStrategyTests() + { + EXPECT_EQ(retry_.attemptNumber(), 0); + EXPECT_EQ(retry_.delayValue(), delay_); + } + + Retry retry_ = makeRetryExponentialBackoff(delay_, maxDelay_, boost::asio::make_strand(ctx)); + testing::MockFunction mockCallback_; +}; + +TEST_F(RetryWithExponentialBackoffStrategyTests, Retry) +{ + retry_.retry(mockCallback_.AsStdFunction()); + + EXPECT_EQ(retry_.attemptNumber(), 0); + + EXPECT_CALL(mockCallback_, Call()); + runContext(); + + EXPECT_EQ(retry_.attemptNumber(), 1); + EXPECT_EQ(retry_.delayValue(), delay_ * 2); +} + +TEST_F(RetryWithExponentialBackoffStrategyTests, Cancel) +{ + retry_.retry(mockCallback_.AsStdFunction()); + retry_.cancel(); + runContext(); + EXPECT_EQ(retry_.attemptNumber(), 0); + + retry_.cancel(); + EXPECT_EQ(retry_.attemptNumber(), 0); +} + +TEST_F(RetryWithExponentialBackoffStrategyTests, Reset) +{ + retry_.retry(mockCallback_.AsStdFunction()); + + EXPECT_CALL(mockCallback_, Call()); + runContext(); + + EXPECT_EQ(retry_.attemptNumber(), 1); + EXPECT_EQ(retry_.delayValue(), delay_ * 2); + + retry_.reset(); + EXPECT_EQ(retry_.attemptNumber(), 0); + EXPECT_EQ(retry_.delayValue(), delay_); +} diff --git a/unittests/util/TestHttpServer.cpp b/unittests/util/TestHttpServer.cpp index c0046834..cac069e2 100644 --- a/unittests/util/TestHttpServer.cpp +++ b/unittests/util/TestHttpServer.cpp @@ -47,7 +47,12 @@ using tcp = boost::asio::ip::tcp; namespace { void -doSession(beast::tcp_stream stream, TestHttpServer::RequestHandler requestHandler, asio::yield_context yield) +doSession( + beast::tcp_stream stream, + TestHttpServer::RequestHandler requestHandler, + asio::yield_context yield, + bool const allowToFail +) { beast::error_code errorCode; @@ -64,6 +69,9 @@ doSession(beast::tcp_stream stream, TestHttpServer::RequestHandler requestHandle if (errorCode == http::error::end_of_stream) return; + if (allowToFail and errorCode) + return; + ASSERT_FALSE(errorCode) << errorCode.message(); auto response = requestHandler(req); @@ -78,6 +86,9 @@ doSession(beast::tcp_stream stream, TestHttpServer::RequestHandler requestHandle // Send the response beast::async_write(stream, std::move(messageGenerator), yield[errorCode]); + if (allowToFail and errorCode) + return; + ASSERT_FALSE(errorCode) << errorCode.message(); if (!keep_alive) { @@ -104,18 +115,21 @@ TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string hos } void -TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler) +TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler, bool const allowToFail) { boost::asio::spawn( acceptor_.get_executor(), - [this, handler = std::move(handler)](asio::yield_context yield) mutable { + [this, allowToFail, handler = std::move(handler)](asio::yield_context yield) mutable { boost::beast::error_code errorCode; tcp::socket socket(this->acceptor_.get_executor()); acceptor_.async_accept(socket, yield[errorCode]); + if (allowToFail and errorCode) + return; + [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }(); - doSession(beast::tcp_stream{std::move(socket)}, std::move(handler), yield); + doSession(beast::tcp_stream{std::move(socket)}, std::move(handler), yield, allowToFail); }, boost::asio::detached ); diff --git a/unittests/util/TestHttpServer.hpp b/unittests/util/TestHttpServer.hpp index a7407d02..b62773ca 100644 --- a/unittests/util/TestHttpServer.hpp +++ b/unittests/util/TestHttpServer.hpp @@ -51,9 +51,10 @@ public: * @note This method schedules to process only one request * * @param handler RequestHandler to use for incoming request + * @param allowToFail if true, the server will not throw an exception if the request fails */ void - handleRequest(RequestHandler handler); + handleRequest(RequestHandler handler, bool allowToFail = false); private: boost::asio::ip::tcp::acceptor acceptor_; diff --git a/unittests/util/TestWsServer.cpp b/unittests/util/TestWsServer.cpp index 6d4d5b80..122d0818 100644 --- a/unittests/util/TestWsServer.cpp +++ b/unittests/util/TestWsServer.cpp @@ -19,6 +19,9 @@ #include "util/TestWsServer.hpp" +#include "util/Expected.hpp" +#include "util/requests/Types.hpp" + #include #include #include @@ -29,6 +32,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,22 +44,16 @@ #include namespace asio = boost::asio; -namespace beast = boost::beast; namespace websocket = boost::beast::websocket; -TestWsConnection::TestWsConnection(asio::ip::tcp::socket&& socket, boost::asio::yield_context yield) - : ws_(std::move(socket)) +TestWsConnection::TestWsConnection(websocket::stream wsStream) : ws_(std::move(wsStream)) { - ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server)); - beast::error_code errorCode; - ws_.async_accept(yield[errorCode]); - [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }(); } std::optional TestWsConnection::send(std::string const& message, boost::asio::yield_context yield) { - beast::error_code errorCode; + boost::beast::error_code errorCode; ws_.async_write(asio::buffer(message), yield[errorCode]); if (errorCode) return errorCode.message(); @@ -64,21 +63,21 @@ TestWsConnection::send(std::string const& message, boost::asio::yield_context yi std::optional TestWsConnection::receive(boost::asio::yield_context yield) { - beast::error_code errorCode; - beast::flat_buffer buffer; + boost::beast::error_code errorCode; + boost::beast::flat_buffer buffer; ws_.async_read(buffer, yield[errorCode]); if (errorCode == websocket::error::closed) return std::nullopt; [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }(); - return beast::buffers_to_string(buffer.data()); + return boost::beast::buffers_to_string(buffer.data()); } std::optional TestWsConnection::close(boost::asio::yield_context yield) { - beast::error_code errorCode; + boost::beast::error_code errorCode; ws_.async_close(websocket::close_code::normal, yield[errorCode]); if (errorCode) return errorCode.message(); @@ -93,23 +92,39 @@ TestWsServer::TestWsServer(asio::io_context& context, std::string const& host, i acceptor_.bind(endpoint); } -TestWsConnection +util::Expected TestWsServer::acceptConnection(asio::yield_context yield) { acceptor_.listen(asio::socket_base::max_listen_connections); - beast::error_code errorCode; + + boost::beast::error_code errorCode; asio::ip::tcp::socket socket(acceptor_.get_executor()); acceptor_.async_accept(socket, yield[errorCode]); - [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }(); - return TestWsConnection(std::move(socket), yield); + if (errorCode) + return util::Unexpected{util::requests::RequestError{"Accept error", errorCode}}; + + boost::beast::websocket::stream ws(std::move(socket)); + ws.set_option(websocket::stream_base::timeout::suggested(boost::beast::role_type::server)); + ws.async_accept(yield[errorCode]); + if (errorCode) + return util::Unexpected{util::requests::RequestError{"Handshake error", errorCode}}; + + return TestWsConnection(std::move(ws)); } void TestWsServer::acceptConnectionAndDropIt(asio::yield_context yield) +{ + acceptConnectionWithoutHandshake(yield); +} + +boost::asio::ip::tcp::socket +TestWsServer::acceptConnectionWithoutHandshake(boost::asio::yield_context yield) { acceptor_.listen(asio::socket_base::max_listen_connections); - beast::error_code errorCode; + boost::beast::error_code errorCode; asio::ip::tcp::socket socket(acceptor_.get_executor()); acceptor_.async_accept(socket, yield[errorCode]); [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }(); + return socket; } diff --git a/unittests/util/TestWsServer.hpp b/unittests/util/TestWsServer.hpp index c075ef9e..73dad199 100644 --- a/unittests/util/TestWsServer.hpp +++ b/unittests/util/TestWsServer.hpp @@ -19,6 +19,9 @@ #pragma once +#include "util/Expected.hpp" +#include "util/requests/Types.hpp" + #include #include #include @@ -36,7 +39,7 @@ public: using SendCallback = std::function; using ReceiveCallback = std::function; - TestWsConnection(boost::asio::ip::tcp::socket&& socket, boost::asio::yield_context yield); + TestWsConnection(boost::beast::websocket::stream wsStream); // returns error message if error occurs std::optional @@ -56,9 +59,12 @@ class TestWsServer { public: TestWsServer(boost::asio::io_context& context, std::string const& host, int port); - TestWsConnection + util::Expected acceptConnection(boost::asio::yield_context yield); void acceptConnectionAndDropIt(boost::asio::yield_context yield); + + boost::asio::ip::tcp::socket + acceptConnectionWithoutHandshake(boost::asio::yield_context yield); }; diff --git a/unittests/util/requests/RequestBuilderTests.cpp b/unittests/util/requests/RequestBuilderTests.cpp index c7f4df00..a7ece3b9 100644 --- a/unittests/util/requests/RequestBuilderTests.cpp +++ b/unittests/util/requests/RequestBuilderTests.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include using namespace util::requests; @@ -48,11 +49,13 @@ struct RequestBuilderTestBundle { std::string target; }; -struct RequestBuilderTest : SyncAsioContextTest, testing::WithParamInterface { +struct RequestBuilderTestBase : SyncAsioContextTest { TestHttpServer server{ctx, "0.0.0.0", 11111}; RequestBuilder builder{"localhost", "11111"}; }; +struct RequestBuilderTest : RequestBuilderTestBase, testing::WithParamInterface {}; + INSTANTIATE_TEST_CASE_P( RequestBuilderTest, RequestBuilderTest, @@ -61,7 +64,9 @@ INSTANTIATE_TEST_CASE_P( RequestBuilderTestBundle{ "GetWithHeaders", http::verb::get, - {{http::field::accept, "text/html"}, {http::field::authorization, "password"}}, + {{http::field::accept, "text/html"}, + {http::field::authorization, "password"}, + {"Custom_header", "some_value"}}, "/" }, RequestBuilderTestBundle{"GetWithTarget", http::verb::get, {}, "/test"}, @@ -69,7 +74,9 @@ INSTANTIATE_TEST_CASE_P( RequestBuilderTestBundle{ "PostWithHeaders", http::verb::post, - {{http::field::accept, "text/html"}, {http::field::authorization, "password"}}, + {{http::field::accept, "text/html"}, + {http::field::authorization, "password"}, + {"Custom_header", "some_value"}}, "/" }, RequestBuilderTestBundle{"PostWithTarget", http::verb::post, {}, "/test"} @@ -86,8 +93,18 @@ TEST_P(RequestBuilderTest, SimpleRequest) server.handleRequest( [&replyBody](http::request request) -> std::optional> { [&]() { - ASSERT_TRUE(request.target() == GetParam().target); - ASSERT_TRUE(request.method() == GetParam().method); + EXPECT_TRUE(request.target() == GetParam().target); + EXPECT_TRUE(request.method() == GetParam().method); + for (auto const& header : GetParam().headers) { + std::visit( + [&](auto const& name) { + auto it = request.find(name); + ASSERT_NE(it, request.end()); + EXPECT_EQ(it->value(), header.value); + }, + header.name + ); + } }(); return http::response{http::status::ok, 11, replyBody}; } @@ -97,14 +114,14 @@ TEST_P(RequestBuilderTest, SimpleRequest) auto const response = [&]() -> util::Expected { switch (GetParam().method) { case http::verb::get: - return builder.get(yield); + return builder.getPlain(yield); case http::verb::post: - return builder.post(yield); + return builder.postPlain(yield); default: return util::Unexpected{RequestError{"Invalid HTTP verb"}}; } }(); - ASSERT_TRUE(response) << response.error().message; + ASSERT_TRUE(response) << response.error().message(); EXPECT_EQ(response.value(), replyBody); }); } @@ -123,7 +140,7 @@ TEST_F(RequestBuilderTest, Timeout) } ); runSpawn([this](asio::yield_context yield) { - auto response = builder.get(yield); + auto response = builder.getPlain(yield); EXPECT_FALSE(response); }); } @@ -147,8 +164,8 @@ TEST_F(RequestBuilderTest, RequestWithBody) ); runSpawn([&](asio::yield_context yield) { - auto const response = builder.get(yield); - ASSERT_TRUE(response) << response.error().message; + auto const response = builder.getPlain(yield); + ASSERT_TRUE(response) << response.error().message(); EXPECT_EQ(response.value(), replyBody) << response.value(); }); } @@ -157,9 +174,9 @@ TEST_F(RequestBuilderTest, ResolveError) { builder = RequestBuilder{"wrong_host", "11111"}; runSpawn([this](asio::yield_context yield) { - auto const response = builder.get(yield); + auto const response = builder.getPlain(yield); ASSERT_FALSE(response); - EXPECT_TRUE(response.error().message.starts_with("Resolve error")) << response.error().message; + EXPECT_TRUE(response.error().message().starts_with("Resolve error")) << response.error().message(); }); } @@ -168,21 +185,75 @@ TEST_F(RequestBuilderTest, ConnectionError) builder = RequestBuilder{"localhost", "11112"}; builder.setTimeout(std::chrono::milliseconds{1}); runSpawn([this](asio::yield_context yield) { - auto const response = builder.get(yield); + auto const response = builder.getPlain(yield); ASSERT_FALSE(response); - EXPECT_TRUE(response.error().message.starts_with("Connection error")) << response.error().message; + EXPECT_TRUE(response.error().message().starts_with("Connection error")) << response.error().message(); }); } -TEST_F(RequestBuilderTest, WritingError) +TEST_F(RequestBuilderTest, ResponseStatusIsNotOk) { + server.handleRequest([](auto&&) -> std::optional> { + return http::response{http::status::not_found, 11, "Not found"}; + }); + + runSpawn([this](asio::yield_context yield) { + auto const response = builder.getPlain(yield); + ASSERT_FALSE(response); + EXPECT_TRUE(response.error().message().starts_with("Response status is not OK")) << response.error().message(); + }); +} + +struct RequestBuilderSslTestBundle { + std::string testName; + boost::beast::http::verb method; +}; + +struct RequestBuilderSslTest : RequestBuilderTestBase, testing::WithParamInterface {}; + +INSTANTIATE_TEST_CASE_P( + RequestBuilderSslTest, + RequestBuilderSslTest, + testing::Values( + RequestBuilderSslTestBundle{"Get", http::verb::get}, + RequestBuilderSslTestBundle{"Post", http::verb::post} + ), + [](auto const& info) { return info.param.testName; } +); + +TEST_P(RequestBuilderSslTest, TrySslUsePlain) +{ + // First try will be SSL, but the server can't handle SSL requests server.handleRequest( - [](http::request request) -> std::optional> { + [](auto&&) -> std::optional> { + []() { FAIL(); }(); + return std::nullopt; + }, + true + ); + + server.handleRequest( + [&](http::request request) -> std::optional> { [&]() { EXPECT_EQ(request.target(), "/"); - EXPECT_EQ(request.method(), http::verb::get); + EXPECT_EQ(request.method(), GetParam().method); }(); - return std::nullopt; + return http::response{http::status::ok, 11, "Hello, world!"}; } ); + + runSpawn([this](asio::yield_context yield) { + auto const response = [&]() -> util::Expected { + switch (GetParam().method) { + case http::verb::get: + return builder.get(yield); + case http::verb::post: + return builder.post(yield); + default: + return util::Unexpected{RequestError{"Invalid HTTP verb"}}; + } + }(); + ASSERT_TRUE(response) << response.error().message(); + EXPECT_EQ(response.value(), "Hello, world!"); + }); } diff --git a/unittests/util/requests/WsConnectionTests.cpp b/unittests/util/requests/WsConnectionTests.cpp index 734bfd56..52bb471d 100644 --- a/unittests/util/requests/WsConnectionTests.cpp +++ b/unittests/util/requests/WsConnectionTests.cpp @@ -17,19 +17,28 @@ */ //============================================================================== +#include "util/Expected.hpp" #include "util/Fixtures.hpp" #include "util/TestWsServer.hpp" #include "util/requests/Types.hpp" #include "util/requests/WsConnection.hpp" +#include +#include #include +#include +#include #include #include +#include #include #include +#include #include +#include #include +#include #include using namespace util::requests; @@ -39,6 +48,14 @@ namespace http = boost::beast::http; struct WsConnectionTestsBase : SyncAsioContextTest { WsConnectionBuilder builder{"localhost", "11112"}; TestWsServer server{ctx, "0.0.0.0", 11112}; + + template + T + unwrap(util::Expected expected) + { + [&]() { ASSERT_TRUE(expected.has_value()) << expected.error().message(); }(); + return std::move(expected).value(); + } }; struct WsConnectionTestBundle { @@ -65,7 +82,9 @@ INSTANTIATE_TEST_CASE_P( WsConnectionTestBundle{"singleHeader", {{http::field::accept, "text/html"}}, std::nullopt}, WsConnectionTestBundle{ "multiple headers", - {{http::field::accept, "text/html"}, {http::field::authorization, "password"}}, + {{http::field::accept, "text/html"}, + {http::field::authorization, "password"}, + {"Custom_header", "some_value"}}, std::nullopt }, WsConnectionTestBundle{"target", {}, "/target"} @@ -74,17 +93,13 @@ INSTANTIATE_TEST_CASE_P( TEST_P(WsConnectionTests, SendAndReceive) { - auto target = GetParam().target; - if (target) { + if (auto const target = GetParam().target; target) { builder.setTarget(*target); } - - for (auto const& header : GetParam().headers) { - builder.addHeader(header); - } + builder.addHeaders(GetParam().headers); asio::spawn(ctx, [&](asio::yield_context yield) { - auto serverConnection = server.acceptConnection(yield); + auto serverConnection = unwrap(server.acceptConnection(yield)); for (size_t i = 0; i < clientMessages.size(); ++i) { auto message = serverConnection.receive(yield); @@ -96,29 +111,58 @@ TEST_P(WsConnectionTests, SendAndReceive) }); runSpawn([&](asio::yield_context yield) { - auto maybeConnection = builder.connect(yield); - ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message; + auto maybeConnection = builder.plainConnect(yield); + ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message(); auto& connection = *maybeConnection; for (size_t i = 0; i < serverMessages.size(); ++i) { auto error = connection->write(clientMessages.at(i), yield); - ASSERT_FALSE(error) << error->message; + ASSERT_FALSE(error) << error->message(); auto message = connection->read(yield); - ASSERT_TRUE(message.has_value()) << message.error().message; + ASSERT_TRUE(message.has_value()) << message.error().message(); EXPECT_EQ(serverMessages.at(i), message.value()); } }); } +TEST_F(WsConnectionTests, TrySslUsePlain) +{ + asio::spawn(ctx, [&](asio::yield_context yield) { + // Client attempts to establish SSL connection first which will fail + auto failedConnection = server.acceptConnection(yield); + EXPECT_FALSE(failedConnection.has_value()); + + auto serverConnection = unwrap(server.acceptConnection(yield)); + auto message = serverConnection.receive(yield); + EXPECT_EQ(message, "hello"); + + auto error = serverConnection.send("goodbye", yield); + EXPECT_FALSE(error) << *error; + }); + + runSpawn([&](asio::yield_context yield) { + auto maybeConnection = builder.connect(yield); + ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message(); + auto& connection = *maybeConnection; + + auto error = connection->write("hello", yield); + ASSERT_FALSE(error) << error->message(); + + auto message = connection->read(yield); + ASSERT_TRUE(message.has_value()) << message.error().message(); + EXPECT_EQ(message.value(), "goodbye"); + }); +} + TEST_F(WsConnectionTests, Timeout) { builder.setConnectionTimeout(std::chrono::milliseconds{1}); runSpawn([&](asio::yield_context yield) { - auto connection = builder.connect(yield); + auto connection = builder.plainConnect(yield); ASSERT_FALSE(connection.has_value()); - EXPECT_TRUE(connection.error().message.starts_with("Connect error")); + EXPECT_TRUE(connection.error().message().starts_with("Connect error")); }); } @@ -126,9 +170,9 @@ TEST_F(WsConnectionTests, ResolveError) { builder = WsConnectionBuilder{"wrong_host", "11112"}; runSpawn([&](asio::yield_context yield) { - auto connection = builder.connect(yield); + auto connection = builder.plainConnect(yield); ASSERT_FALSE(connection.has_value()); - EXPECT_TRUE(connection.error().message.starts_with("Resolve error")) << connection.error().message; + EXPECT_TRUE(connection.error().message().starts_with("Resolve error")) << connection.error().message(); }); } @@ -137,27 +181,55 @@ TEST_F(WsConnectionTests, WsHandshakeError) builder.setConnectionTimeout(std::chrono::milliseconds{1}); asio::spawn(ctx, [&](asio::yield_context yield) { server.acceptConnectionAndDropIt(yield); }); runSpawn([&](asio::yield_context yield) { - auto connection = builder.connect(yield); + auto connection = builder.plainConnect(yield); ASSERT_FALSE(connection.has_value()); - EXPECT_TRUE(connection.error().message.starts_with("Handshake error")) << connection.error().message; + EXPECT_TRUE(connection.error().message().starts_with("Handshake error")) << connection.error().message(); + }); +} + +TEST_F(WsConnectionTests, WsHandshakeTimeout) +{ + builder.setWsHandshakeTimeout(std::chrono::milliseconds{1}); + asio::spawn(ctx, [&](asio::yield_context yield) { + auto socket = server.acceptConnectionWithoutHandshake(yield); + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + }); + runSpawn([&](asio::yield_context yield) { + auto connection = builder.plainConnect(yield); + ASSERT_FALSE(connection.has_value()); + EXPECT_TRUE(connection.error().message().starts_with("Handshake error")) << connection.error().message(); }); } TEST_F(WsConnectionTests, CloseConnection) { asio::spawn(ctx, [&](asio::yield_context yield) { - auto serverConnection = server.acceptConnection(yield); + auto serverConnection = unwrap(server.acceptConnection(yield)); auto message = serverConnection.receive(yield); EXPECT_EQ(std::nullopt, message); }); runSpawn([&](asio::yield_context yield) { - auto connection = builder.connect(yield); - ASSERT_TRUE(connection.has_value()) << connection.error().message; + auto connection = unwrap(builder.plainConnect(yield)); - auto error = connection->operator*().close(yield); - EXPECT_FALSE(error.has_value()) << error->message; + auto error = connection->close(yield); + EXPECT_FALSE(error.has_value()) << error->message(); + }); +} + +TEST_F(WsConnectionTests, CloseConnectionTimeout) +{ + asio::spawn(ctx, [&](asio::yield_context yield) { + auto serverConnection = unwrap(server.acceptConnection(yield)); + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + }); + + runSpawn([&](asio::yield_context yield) { + auto connection = unwrap(builder.plainConnect(yield)); + + auto error = connection->close(yield, std::chrono::milliseconds{1}); + EXPECT_TRUE(error.has_value()); }); } @@ -165,7 +237,7 @@ TEST_F(WsConnectionTests, MultipleConnections) { for (size_t i = 0; i < 2; ++i) { asio::spawn(ctx, [&](asio::yield_context yield) { - auto serverConnection = server.acceptConnection(yield); + auto serverConnection = unwrap(server.acceptConnection(yield)); auto message = serverConnection.receive(yield); ASSERT_TRUE(message.has_value()); @@ -173,11 +245,11 @@ TEST_F(WsConnectionTests, MultipleConnections) }); runSpawn([&](asio::yield_context yield) { - auto connection = builder.connect(yield); - ASSERT_TRUE(connection.has_value()) << connection.error().message; + auto connection = builder.plainConnect(yield); + ASSERT_TRUE(connection.has_value()) << connection.error().message(); auto error = connection->operator*().write("hello", yield); - ASSERT_FALSE(error) << error->message; + ASSERT_FALSE(error) << error->message(); }); } } @@ -201,22 +273,22 @@ INSTANTIATE_TEST_SUITE_P( } ); -TEST_P(WsConnectionErrorTests, WriteError) +TEST_P(WsConnectionErrorTests, ReadWriteError) { asio::spawn(ctx, [&](asio::yield_context yield) { - auto serverConnection = server.acceptConnection(yield); + auto serverConnection = unwrap(server.acceptConnection(yield)); auto error = serverConnection.close(yield); EXPECT_FALSE(error.has_value()) << *error; }); runSpawn([&](asio::yield_context yield) { - auto maybeConnection = builder.connect(yield); - ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message; + auto maybeConnection = builder.plainConnect(yield); + ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message(); auto& connection = *maybeConnection; auto error = connection->close(yield); - EXPECT_FALSE(error.has_value()) << error->message; + EXPECT_FALSE(error.has_value()) << error->message(); switch (GetParam()) { case WsConnectionErrorTestsBundle::Read: { diff --git a/unittests/web/ServerTests.cpp b/unittests/web/ServerTests.cpp index bf30f1a5..f89e657f 100644 --- a/unittests/web/ServerTests.cpp +++ b/unittests/web/ServerTests.cpp @@ -166,12 +166,6 @@ protected: runner.emplace([this] { ctx.run(); }); } - void - SetUp() override - { - NoLoggerFixture::SetUp(); - } - // this ctx is for dos timer boost::asio::io_context ctxSync; Config cfg{boost::json::parse(JSONData)};