diff --git a/CMakeLists.txt b/CMakeLists.txt index d8b06e48..a62836cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,10 @@ target_sources (clio PRIVATE src/etl/impl/ForwardCache.cpp ## Feed src/feed/SubscriptionManager.cpp + src/feed/impl/TransactionFeed.cpp + src/feed/impl/LedgerFeed.cpp + src/feed/impl/ProposedTransactionFeed.cpp + src/feed/impl/SingleFeedBase.cpp ## Web src/web/impl/AdminVerificationStrategy.cpp src/web/IntervalSweepHandler.cpp @@ -181,8 +185,6 @@ if (tests) unittests/ProfilerTests.cpp unittests/JsonUtilTests.cpp unittests/DOSGuardTests.cpp - unittests/SubscriptionTests.cpp - unittests/SubscriptionManagerTests.cpp unittests/util/AssertTests.cpp unittests/util/BatchingTests.cpp unittests/util/TestObject.cpp @@ -260,7 +262,16 @@ if (tests) unittests/web/ServerTests.cpp unittests/web/RPCServerHandlerTests.cpp unittests/web/WhitelistHandlerTests.cpp - unittests/web/SweepHandlerTests.cpp) + unittests/web/SweepHandlerTests.cpp + # Feed + unittests/feed/SubscriptionManagerTests.cpp + unittests/feed/SingleFeedBaseTests.cpp + unittests/feed/ProposedTransactionFeedTests.cpp + unittests/feed/BookChangesFeedTests.cpp + unittests/feed/LedgerFeedTests.cpp + unittests/feed/TransactionFeedTests.cpp + unittests/feed/ForwardFeedTests.cpp + unittests/feed/TrackableSignalTests.cpp) include (CMake/deps/gtest.cmake) diff --git a/src/feed/SubscriptionManager.cpp b/src/feed/SubscriptionManager.cpp index 2bcc66bd..bafad533 100644 --- a/src/feed/SubscriptionManager.cpp +++ b/src/feed/SubscriptionManager.cpp @@ -19,169 +19,82 @@ #include "feed/SubscriptionManager.h" -#include "data/BackendInterface.h" #include "data/Types.h" -#include "rpc/BookChangesHelper.h" -#include "rpc/JS.h" -#include "rpc/RPCHelpers.h" -#include "util/Assert.h" +#include "feed/Types.h" -#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 { - void -Subscription::subscribe(SessionPtrType const& session) +SubscriptionManager::subBookChanges(SubscriberSharedPtr const& subscriber) { - boost::asio::post(strand_, [this, session]() { addSession(session, subscribers_, subCount_); }); + bookChangesFeed_.sub(subscriber); } void -Subscription::unsubscribe(SessionPtrType const& session) +SubscriptionManager::unsubBookChanges(SubscriberSharedPtr const& subscriber) { - boost::asio::post(strand_, [this, session]() { removeSession(session, subscribers_, subCount_); }); -} - -bool -Subscription::hasSession(SessionPtrType const& session) -{ - return subscribers_.contains(session); + bookChangesFeed_.unsub(subscriber); } void -Subscription::publish(std::shared_ptr const& message) -{ - boost::asio::post(strand_, [this, message]() { sendToSubscribers(message, subscribers_, subCount_); }); -} - -boost::json::object -getLedgerPubMessage( +SubscriptionManager::pubBookChanges( ripple::LedgerHeader const& lgrInfo, - ripple::Fees const& fees, - std::string const& ledgerRange, - std::uint32_t txnCount -) + std::vector const& transactions +) const { - boost::json::object pubMsg; + bookChangesFeed_.pub(lgrInfo, transactions); +} - pubMsg["type"] = "ledgerClosed"; - pubMsg["ledger_index"] = lgrInfo.seq; - pubMsg["ledger_hash"] = to_string(lgrInfo.hash); - pubMsg["ledger_time"] = lgrInfo.closeTime.time_since_epoch().count(); +void +SubscriptionManager::subProposedTransactions(SubscriberSharedPtr const& subscriber) +{ + proposedTransactionFeed_.sub(subscriber); +} - pubMsg["fee_base"] = rpc::toBoostJson(fees.base.jsonClipped()); - pubMsg["reserve_base"] = rpc::toBoostJson(fees.reserve.jsonClipped()); - pubMsg["reserve_inc"] = rpc::toBoostJson(fees.increment.jsonClipped()); +void +SubscriptionManager::unsubProposedTransactions(SubscriberSharedPtr const& subscriber) +{ + proposedTransactionFeed_.unsub(subscriber); +} - pubMsg["validated_ledgers"] = ledgerRange; - pubMsg["txn_count"] = txnCount; - return pubMsg; +void +SubscriptionManager::subProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) +{ + proposedTransactionFeed_.sub(account, subscriber); +} + +void +SubscriptionManager::unsubProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) +{ + proposedTransactionFeed_.unsub(account, subscriber); +} + +void +SubscriptionManager::forwardProposedTransaction(boost::json::object const& receivedTxJson) +{ + proposedTransactionFeed_.pub(receivedTxJson); } boost::json::object -SubscriptionManager::subLedger(boost::asio::yield_context yield, SessionPtrType session) +SubscriptionManager::subLedger(boost::asio::yield_context yield, SubscriberSharedPtr const& subscriber) { - subscribeHelper(session, ledgerSubscribers_, [this](SessionPtrType session) { unsubLedger(session); }); - - auto ledgerRange = backend_->fetchLedgerRange(); - ASSERT(ledgerRange.has_value(), "Ledger range must be valid"); - auto lgrInfo = backend_->fetchLedgerBySequence(ledgerRange->maxSequence, yield); - ASSERT(lgrInfo.has_value(), "Ledger must be valid"); - - std::optional fees; - fees = backend_->fetchFees(lgrInfo->seq, yield); - ASSERT(fees.has_value(), "Fees must be valid"); - - std::string const range = std::to_string(ledgerRange->minSequence) + "-" + std::to_string(ledgerRange->maxSequence); - - auto pubMsg = getLedgerPubMessage(*lgrInfo, *fees, range, 0); - pubMsg.erase("txn_count"); - pubMsg.erase("type"); - return pubMsg; + return ledgerFeed_.sub(yield, backend_, subscriber); } void -SubscriptionManager::unsubLedger(SessionPtrType session) +SubscriptionManager::unsubLedger(SubscriberSharedPtr const& subscriber) { - ledgerSubscribers_.unsubscribe(session); -} - -void -SubscriptionManager::subTransactions(SessionPtrType session) -{ - subscribeHelper(session, txSubscribers_, [this](SessionPtrType session) { unsubTransactions(session); }); -} - -void -SubscriptionManager::unsubTransactions(SessionPtrType session) -{ - txSubscribers_.unsubscribe(session); -} - -void -SubscriptionManager::subAccount(ripple::AccountID const& account, SessionPtrType const& session) -{ - subscribeHelper(session, account, accountSubscribers_, [this, account](SessionPtrType session) { - unsubAccount(account, session); - }); -} - -void -SubscriptionManager::unsubAccount(ripple::AccountID const& account, SessionPtrType const& session) -{ - accountSubscribers_.unsubscribe(session, account); -} - -void -SubscriptionManager::subBook(ripple::Book const& book, SessionPtrType session) -{ - subscribeHelper(session, book, bookSubscribers_, [this, book](SessionPtrType session) { - unsubBook(book, session); - }); -} - -void -SubscriptionManager::unsubBook(ripple::Book const& book, SessionPtrType session) -{ - bookSubscribers_.unsubscribe(session, book); -} - -void -SubscriptionManager::subBookChanges(SessionPtrType session) -{ - subscribeHelper(session, bookChangesSubscribers_, [this](SessionPtrType session) { unsubBookChanges(session); }); -} - -void -SubscriptionManager::unsubBookChanges(SessionPtrType session) -{ - bookChangesSubscribers_.unsubscribe(session); + ledgerFeed_.unsub(subscriber); } void @@ -189,230 +102,96 @@ SubscriptionManager::pubLedger( ripple::LedgerHeader const& lgrInfo, ripple::Fees const& fees, std::string const& ledgerRange, - std::uint32_t txnCount + std::uint32_t const txnCount +) const +{ + ledgerFeed_.pub(lgrInfo, fees, ledgerRange, txnCount); +} + +void +SubscriptionManager::subManifest(SubscriberSharedPtr const& subscriber) +{ + manifestFeed_.sub(subscriber); +} + +void +SubscriptionManager::unsubManifest(SubscriberSharedPtr const& subscriber) +{ + manifestFeed_.unsub(subscriber); +} + +void +SubscriptionManager::forwardManifest(boost::json::object const& manifestJson) const +{ + manifestFeed_.pub(manifestJson); +} + +void +SubscriptionManager::subValidation(SubscriberSharedPtr const& subscriber) +{ + validationsFeed_.sub(subscriber); +} + +void +SubscriptionManager::unsubValidation(SubscriberSharedPtr const& subscriber) +{ + validationsFeed_.unsub(subscriber); +} + +void +SubscriptionManager::forwardValidation(boost::json::object const& validationJson) const +{ + validationsFeed_.pub(validationJson); +} + +void +SubscriptionManager::subTransactions(SubscriberSharedPtr const& subscriber, std::uint32_t const apiVersion) +{ + transactionFeed_.sub(subscriber, apiVersion); +} + +void +SubscriptionManager::unsubTransactions(SubscriberSharedPtr const& subscriber) +{ + transactionFeed_.unsub(subscriber); +} + +void +SubscriptionManager::subAccount( + ripple::AccountID const& account, + SubscriberSharedPtr const& subscriber, + std::uint32_t const apiVersion ) { - auto message = - std::make_shared(boost::json::serialize(getLedgerPubMessage(lgrInfo, fees, ledgerRange, txnCount)) - ); - - ledgerSubscribers_.publish(message); + transactionFeed_.sub(account, subscriber, apiVersion); } void -SubscriptionManager::pubTransaction(data::TransactionAndMetadata const& blobs, ripple::LedgerHeader const& lgrInfo) +SubscriptionManager::unsubAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) { - auto [tx, meta] = rpc::deserializeTxPlusMeta(blobs, lgrInfo.seq); - boost::json::object pubObj; - pubObj[JS(transaction)] = rpc::toJson(*tx); - pubObj[JS(meta)] = rpc::toJson(*meta); - rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, blobs.date); - // hardcode api_version to 1 for now, until https://github.com/XRPLF/clio/issues/978 fixed - rpc::insertDeliverMaxAlias(pubObj[JS(transaction)].as_object(), 1); - pubObj[JS(type)] = "transaction"; - pubObj[JS(validated)] = true; - pubObj[JS(status)] = "closed"; - pubObj[JS(close_time_iso)] = ripple::to_string_iso(lgrInfo.closeTime); - - pubObj[JS(ledger_index)] = lgrInfo.seq; - pubObj[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); - pubObj[JS(transaction)].as_object()[JS(date)] = lgrInfo.closeTime.time_since_epoch().count(); - - pubObj[JS(engine_result_code)] = meta->getResult(); - std::string token; - std::string human; - ripple::transResultInfo(meta->getResultTER(), token, human); - pubObj[JS(engine_result)] = token; - pubObj[JS(engine_result_message)] = human; - if (tx->getTxnType() == ripple::ttOFFER_CREATE) { - auto account = tx->getAccountID(ripple::sfAccount); - auto amount = tx->getFieldAmount(ripple::sfTakerGets); - if (account != amount.issue().account) { - ripple::STAmount ownerFunds; - auto fetchFundsSynchronous = [&]() { - data::synchronous([&](boost::asio::yield_context yield) { - ownerFunds = rpc::accountFunds(*backend_, lgrInfo.seq, amount, account, yield); - }); - }; - - data::retryOnTimeout(fetchFundsSynchronous); - - pubObj[JS(transaction)].as_object()[JS(owner_funds)] = ownerFunds.getText(); - } - } - - auto pubMsg = std::make_shared(boost::json::serialize(pubObj)); - txSubscribers_.publish(pubMsg); - - auto accounts = meta->getAffectedAccounts(); - - for (auto const& account : accounts) - accountSubscribers_.publish(pubMsg, account); - - std::unordered_set alreadySent; - - for (auto const& node : meta->getNodes()) { - if (node.getFieldU16(ripple::sfLedgerEntryType) == ripple::ltOFFER) { - ripple::SField const* field = nullptr; - - // We need a field that contains the TakerGets and TakerPays - // parameters. - if (node.getFName() == ripple::sfModifiedNode) { - field = &ripple::sfPreviousFields; - } else if (node.getFName() == ripple::sfCreatedNode) { - field = &ripple::sfNewFields; - } else if (node.getFName() == ripple::sfDeletedNode) { - field = &ripple::sfFinalFields; - } - - if (field != nullptr) { - auto data = dynamic_cast(node.peekAtPField(*field)); - - if ((data != nullptr) && data->isFieldPresent(ripple::sfTakerPays) && - data->isFieldPresent(ripple::sfTakerGets)) { - // determine the OrderBook - ripple::Book const book{ - data->getFieldAmount(ripple::sfTakerGets).issue(), - data->getFieldAmount(ripple::sfTakerPays).issue() - }; - if (alreadySent.find(book) == alreadySent.end()) { - bookSubscribers_.publish(pubMsg, book); - alreadySent.insert(book); - } - } - } - } - } + transactionFeed_.unsub(account, subscriber); } void -SubscriptionManager::pubBookChanges( - ripple::LedgerHeader const& lgrInfo, - std::vector const& transactions +SubscriptionManager::subBook( + ripple::Book const& book, + SubscriberSharedPtr const& subscriber, + std::uint32_t const apiVersion ) { - auto const json = rpc::computeBookChanges(lgrInfo, transactions); - auto const bookChangesMsg = std::make_shared(boost::json::serialize(json)); - bookChangesSubscribers_.publish(bookChangesMsg); + transactionFeed_.sub(book, subscriber, apiVersion); } void -SubscriptionManager::forwardProposedTransaction(boost::json::object const& response) +SubscriptionManager::unsubBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber) { - auto pubMsg = std::make_shared(boost::json::serialize(response)); - txProposedSubscribers_.publish(pubMsg); - - auto transaction = response.at("transaction").as_object(); - auto accounts = rpc::getAccountsFromTransaction(transaction); - - for (ripple::AccountID const& account : accounts) - accountProposedSubscribers_.publish(pubMsg, account); + transactionFeed_.unsub(book, subscriber); } void -SubscriptionManager::forwardManifest(boost::json::object const& response) +SubscriptionManager::pubTransaction(data::TransactionAndMetadata const& txMeta, ripple::LedgerHeader const& lgrInfo) { - auto pubMsg = std::make_shared(boost::json::serialize(response)); - manifestSubscribers_.publish(pubMsg); -} - -void -SubscriptionManager::forwardValidation(boost::json::object const& response) -{ - auto pubMsg = std::make_shared(boost::json::serialize(response)); - validationsSubscribers_.publish(pubMsg); -} - -void -SubscriptionManager::subProposedAccount(ripple::AccountID const& account, SessionPtrType session) -{ - subscribeHelper(session, account, accountProposedSubscribers_, [this, account](SessionPtrType session) { - unsubProposedAccount(account, session); - }); -} - -void -SubscriptionManager::subManifest(SessionPtrType session) -{ - subscribeHelper(session, manifestSubscribers_, [this](SessionPtrType session) { unsubManifest(session); }); -} - -void -SubscriptionManager::unsubManifest(SessionPtrType session) -{ - manifestSubscribers_.unsubscribe(session); -} - -void -SubscriptionManager::subValidation(SessionPtrType session) -{ - subscribeHelper(session, validationsSubscribers_, [this](SessionPtrType session) { unsubValidation(session); }); -} - -void -SubscriptionManager::unsubValidation(SessionPtrType session) -{ - validationsSubscribers_.unsubscribe(session); -} - -void -SubscriptionManager::unsubProposedAccount(ripple::AccountID const& account, SessionPtrType session) -{ - accountProposedSubscribers_.unsubscribe(session, account); -} - -void -SubscriptionManager::subProposedTransactions(SessionPtrType session) -{ - subscribeHelper(session, txProposedSubscribers_, [this](SessionPtrType session) { - unsubProposedTransactions(session); - }); -} - -void -SubscriptionManager::unsubProposedTransactions(SessionPtrType session) -{ - txProposedSubscribers_.unsubscribe(session); -} - -void -SubscriptionManager::subscribeHelper(SessionPtrType const& session, Subscription& subs, CleanupFunction&& func) -{ - if (subs.hasSession(session)) - return; - subs.subscribe(session); - std::scoped_lock const lk(cleanupMtx_); - cleanupFuncs_[session].push_back(std::move(func)); -} - -template -void -SubscriptionManager::subscribeHelper( - SessionPtrType const& session, - Key const& k, - SubscriptionMap& subs, - CleanupFunction&& func -) -{ - if (subs.hasSession(session, k)) - return; - subs.subscribe(session, k); - std::scoped_lock const lk(cleanupMtx_); - cleanupFuncs_[session].push_back(std::move(func)); -} - -void -SubscriptionManager::cleanup(SessionPtrType session) -{ - std::scoped_lock const lk(cleanupMtx_); - if (!cleanupFuncs_.contains(session)) - return; - - for (auto const& f : cleanupFuncs_[session]) { - f(session); - } - - cleanupFuncs_.erase(session); + transactionFeed_.pub(txMeta, lgrInfo, backend_); } } // namespace feed diff --git a/src/feed/SubscriptionManager.h b/src/feed/SubscriptionManager.h index 75ec2109..140c9ca2 100644 --- a/src/feed/SubscriptionManager.h +++ b/src/feed/SubscriptionManager.h @@ -20,369 +20,140 @@ #pragma once #include "data/BackendInterface.h" -#include "util/config/Config.h" +#include "data/Types.h" +#include "feed/Types.h" +#include "feed/impl/BookChangesFeed.h" +#include "feed/impl/ForwardFeed.h" +#include "feed/impl/LedgerFeed.h" +#include "feed/impl/ProposedTransactionFeed.h" +#include "feed/impl/TransactionFeed.h" #include "util/log/Logger.h" -#include "util/prometheus/Prometheus.h" -#include "web/interface/ConnectionBase.h" -#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include -/** - * @brief This namespace deals with subscriptions. - */ namespace feed { -using SessionPtrType = std::shared_ptr; - -/** - * @brief Sends a message to subscribers. - * - * @param message The message to send - * @param subscribers The subscription stream to send the message to - * @param counter The subscription counter to decrement if session is detected as dead - */ -template -inline void -sendToSubscribers(std::shared_ptr const& message, T& subscribers, util::prometheus::GaugeInt& counter) -{ - for (auto it = subscribers.begin(); it != subscribers.end();) { - auto& session = *it; - if (session->dead()) { - it = subscribers.erase(it); - --counter; - } else { - session->send(message); - ++it; - } - } -} - -/** - * @brief Adds a session to the subscription stream. - * - * @param session The session to add - * @param subscribers The stream to subscribe to - * @param counter The counter representing the current total subscribers - */ -template -inline void -addSession(SessionPtrType session, T& subscribers, util::prometheus::GaugeInt& counter) -{ - if (!subscribers.contains(session)) { - subscribers.insert(session); - ++counter; - } -} - -/** - * @brief Removes a session from the subscription stream. - * - * @param session The session to remove - * @param subscribers The stream to unsubscribe from - * @param counter The counter representing the current total subscribers - */ -template -inline void -removeSession(SessionPtrType session, T& subscribers, util::prometheus::GaugeInt& counter) -{ - if (subscribers.contains(session)) { - subscribers.erase(session); - --counter; - } -} - -/** - * @brief Represents a subscription stream. - */ -class Subscription { - boost::asio::strand strand_; - std::unordered_set subscribers_ = {}; - util::prometheus::GaugeInt& subCount_; - -public: - Subscription() = delete; - Subscription(Subscription&) = delete; - Subscription(Subscription&&) = delete; - - /** - * @brief Create a new subscription stream. - * - * @param ioc The io_context to run on - */ - explicit Subscription(boost::asio::io_context& ioc, std::string const& name) - : strand_(boost::asio::make_strand(ioc)) - , subCount_(PrometheusService::gaugeInt( - "subscriptions_current_number", - util::prometheus::Labels({util::prometheus::Label{"stream", name}}), - fmt::format("Current subscribers number on the {} stream", name) - )) - { - } - - ~Subscription() = default; - - /** - * @brief Adds the given session to the subscribers set. - * - * @param session The session to add - */ - void - subscribe(SessionPtrType const& session); - - /** - * @brief Removes the given session from the subscribers set. - * - * @param session The session to remove - */ - void - unsubscribe(SessionPtrType const& session); - - /** - * @brief Check if a session has been in subscribers list. - * - * @param session The session to check - * @return true if the session is in the subscribers list; false otherwise - */ - bool - hasSession(SessionPtrType const& session); - - /** - * @brief Sends the given message to all subscribers. - * - * @param message The message to send - */ - void - publish(std::shared_ptr const& message); - - /** - * @return Total subscriber count on this stream. - */ - std::uint64_t - count() const - { - return subCount_.value(); - } - - /** - * @return true if the stream currently has no subscribers; false otherwise - */ - bool - empty() const - { - return count() == 0; - } -}; - -/** - * @brief Represents a collection of subscriptions where each stream is mapped to a key. - */ -template -class SubscriptionMap { - using SubscribersType = std::set; - - boost::asio::strand strand_; - std::unordered_map subscribers_ = {}; - util::prometheus::GaugeInt& subCount_; - -public: - SubscriptionMap() = delete; - SubscriptionMap(SubscriptionMap&) = delete; - SubscriptionMap(SubscriptionMap&&) = delete; - - /** - * @brief Create a new subscription map. - * - * @param ioc The io_context to run on - */ - explicit SubscriptionMap(boost::asio::io_context& ioc, std::string const& name) - : strand_(boost::asio::make_strand(ioc)) - , subCount_(PrometheusService::gaugeInt( - "subscriptions_current_number", - util::prometheus::Labels({util::prometheus::Label{"collection", name}}), - fmt::format("Current subscribers number on the {} collection", name) - )) - { - } - - ~SubscriptionMap() = default; - - /** - * @brief Subscribe to a specific stream by its key. - * - * @param session The session to add - * @param key The key for the subscription to subscribe to - */ - void - subscribe(SessionPtrType const& session, Key const& key) - { - boost::asio::post(strand_, [this, session, key]() { addSession(session, subscribers_[key], subCount_); }); - } - - /** - * @brief Unsubscribe from a specific stream by its key. - * - * @param session The session to remove - * @param key The key for the subscription to unsubscribe from - */ - void - unsubscribe(SessionPtrType const& session, Key const& key) - { - boost::asio::post(strand_, [this, key, session]() { - if (!subscribers_.contains(key)) - return; - - if (!subscribers_[key].contains(session)) - return; - - --subCount_; - subscribers_[key].erase(session); - - if (subscribers_[key].size() == 0) { - subscribers_.erase(key); - } - }); - } - - /** - * @brief Check if a session has been in subscribers list. - * - * @param session The session to check - * @param key The key for the subscription to check - * @return true if the session is in the subscribers list; false otherwise - */ - bool - hasSession(SessionPtrType const& session, Key const& key) - { - if (!subscribers_.contains(key)) - return false; - - return subscribers_[key].contains(session); - } - - /** - * @brief Sends the given message to all subscribers. - * - * @param message The message to send - * @param key The key for the subscription to send the message to - */ - void - publish(std::shared_ptr const& message, Key const& key) - { - boost::asio::post(strand_, [this, key, message]() { - if (!subscribers_.contains(key)) - return; - - sendToSubscribers(message, subscribers_[key], subCount_); - }); - } - - /** - * @return Total subscriber count on all streams in the collection. - */ - std::uint64_t - count() const - { - return subCount_.value(); - } -}; - -/** - * @brief Manages subscriptions. - */ class SubscriptionManager { - util::Logger log_{"Subscriptions"}; - - std::vector workers_; - boost::asio::io_context ioc_; - std::optional work_; - - Subscription ledgerSubscribers_; - Subscription txSubscribers_; - Subscription txProposedSubscribers_; - Subscription manifestSubscribers_; - Subscription validationsSubscribers_; - Subscription bookChangesSubscribers_; - - SubscriptionMap accountSubscribers_; - SubscriptionMap accountProposedSubscribers_; - SubscriptionMap bookSubscribers_; - + std::reference_wrapper ioContext_; std::shared_ptr backend_; + impl::ForwardFeed manifestFeed_; + impl::ForwardFeed validationsFeed_; + impl::LedgerFeed ledgerFeed_; + impl::BookChangesFeed bookChangesFeed_; + impl::TransactionFeed transactionFeed_; + impl::ProposedTransactionFeed proposedTransactionFeed_; + public: - /** - * @brief A factory function that creates a new subscription manager configured from the config provided. - * - * @param config The configuration to use - * @param backend The backend to use - */ - static std::shared_ptr - make_SubscriptionManager(util::Config const& config, std::shared_ptr const& backend) - { - auto numThreads = config.valueOr("subscription_workers", 1); - return std::make_shared(numThreads, backend); - } - - /** - * @brief Creates a new instance of the subscription manager. - * - * @param numThreads The number of worker threads to manage subscriptions - * @param backend The backend to use - */ - SubscriptionManager(std::uint64_t numThreads, std::shared_ptr const& backend) - : ledgerSubscribers_(ioc_, "ledger") - , txSubscribers_(ioc_, "tx") - , txProposedSubscribers_(ioc_, "tx_proposed") - , manifestSubscribers_(ioc_, "manifest") - , validationsSubscribers_(ioc_, "validations") - , bookChangesSubscribers_(ioc_, "book_changes") - , accountSubscribers_(ioc_, "account") - , accountProposedSubscribers_(ioc_, "account_proposed") - , bookSubscribers_(ioc_, "book") + SubscriptionManager( + boost::asio::io_context& ioContext, + std::shared_ptr const& backend + ) + : ioContext_(ioContext) , backend_(backend) + , manifestFeed_(ioContext, "manifest") + , validationsFeed_(ioContext, "validations") + , ledgerFeed_(ioContext) + , bookChangesFeed_(ioContext) + , transactionFeed_(ioContext) + , proposedTransactionFeed_(ioContext) { - work_.emplace(ioc_); - - // We will eventually want to clamp this to be the number of strands, - // since adding more threads than we have strands won't see any - // performance benefits - LOG(log_.info()) << "Starting subscription manager with " << numThreads << " workers"; - - workers_.reserve(numThreads); - for (auto i = numThreads; i > 0; --i) - workers_.emplace_back([this] { ioc_.run(); }); - } - - /** @brief Stops the worker threads of the subscription manager. */ - ~SubscriptionManager() - { - work_.reset(); - - ioc_.stop(); - for (auto& worker : workers_) - worker.join(); } /** - * @brief Subscribe to the ledger stream. - * - * @param yield The coroutine context - * @param session The session to subscribe to the stream - * @return JSON object representing the first message to be sent to the new subscriber + * @brief Subscribe to the book changes feed. + * @param subscriber + */ + void + subBookChanges(SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the book changes feed. + * @param subscriber + */ + void + unsubBookChanges(SubscriberSharedPtr const& subscriber); + + /** + * @brief Publish the book changes feed. + * @param lgrInfo The current ledger header. + * @param transactions The transactions in the current ledger. + */ + void + pubBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector const& transactions) + const; + + /** + * @brief Subscribe to the proposed transactions feed. + * @param subscriber + */ + void + subProposedTransactions(SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the proposed transactions feed. + * @param subscriber + */ + void + unsubProposedTransactions(SubscriberSharedPtr const& subscriber); + + /** + * @brief Subscribe to the proposed transactions feed, only receive the feed when particular account is affected. + * @param account The account to watch. + * @param subscriber + */ + void + subProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the proposed transactions feed for particular account. + * @param account The account to stop watching. + * @param subscriber + */ + void + unsubProposedAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); + + /** + * @brief Forward the proposed transactions feed. + * @param receivedTxJson The proposed transaction json. + */ + void + forwardProposedTransaction(boost::json::object const& receivedTxJson); + + /** + * @brief Subscribe to the ledger feed. + * @param subscriber */ boost::json::object - subLedger(boost::asio::yield_context yield, SessionPtrType session); + subLedger(boost::asio::yield_context yield, SubscriberSharedPtr const& subscriber); /** - * @brief Publish to the ledger stream. - * - * @param lgrInfo The ledger header to serialize - * @param fees The fees to serialize - * @param ledgerRange The ledger range this message applies to - * @param txnCount The total number of transactions to serialize + * @brief Unsubscribe to the ledger feed. + * @param subscriber + */ + void + unsubLedger(SubscriberSharedPtr const& subscriber); + + /** + * @brief Publish the ledger feed. + * @param lgrInfo The ledger header. + * @param fees The fees. + * @param ledgerRange The ledger range. + * @param txnCount The transaction count. */ void pubLedger( @@ -390,232 +161,160 @@ public: ripple::Fees const& fees, std::string const& ledgerRange, std::uint32_t txnCount - ); + ) const; /** - * @brief Publish to the book changes stream. - * - * @param lgrInfo The ledger header to serialize - * @param transactions The transactions to serialize + * @brief Subscribe to the manifest feed. + * @param subscriber */ void - pubBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector const& transactions); + subManifest(SubscriberSharedPtr const& subscriber); /** - * @brief Unsubscribe from the ledger stream. - * - * @param session The session to unsubscribe from the stream + * @brief Unsubscribe to the manifest feed. + * @param subscriber */ void - unsubLedger(SessionPtrType session); + unsubManifest(SubscriberSharedPtr const& subscriber); /** - * @brief Subscribe to the transactions stream. - * - * @param session The session to subscribe to the stream + * @brief Forward the manifest feed. + * @param manifestJson The manifest json to forward. */ void - subTransactions(SessionPtrType session); + forwardManifest(boost::json::object const& manifestJson) const; /** - * @brief Unsubscribe from the transactions stream. - * - * @param session The session to unsubscribe from the stream + * @brief Subscribe to the validation feed. + * @param subscriber */ void - unsubTransactions(SessionPtrType session); + subValidation(SubscriberSharedPtr const& subscriber); /** - * @brief Publish to the book changes stream. - * - * @param blobs The transactions to serialize - * @param lgrInfo The ledger header to serialize + * @brief Unsubscribe to the validation feed. + * @param subscriber */ void - pubTransaction(data::TransactionAndMetadata const& blobs, ripple::LedgerHeader const& lgrInfo); + unsubValidation(SubscriberSharedPtr const& subscriber); /** - * @brief Subscribe to the account changes stream. - * - * @param account The account to monitor changes for - * @param session The session to subscribe to the stream + * @brief Forward the validation feed. + * @param validationJson The validation feed json to forward. */ void - subAccount(ripple::AccountID const& account, SessionPtrType const& session); + forwardValidation(boost::json::object const& validationJson) const; /** - * @brief Unsubscribe from the account changes stream. - * - * @param account The account the stream is for - * @param session The session to unsubscribe from the stream + * @brief Subscribe to the transactions feed. + * @param subscriber + * @param apiVersion The api version of feed to subscribe. */ void - unsubAccount(ripple::AccountID const& account, SessionPtrType const& session); + subTransactions(SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); /** - * @brief Subscribe to a specific book changes stream. - * - * @param book The book to monitor changes for - * @param session The session to subscribe to the stream + * @brief Unsubscribe to the transactions feed. + * @param subscriber */ void - subBook(ripple::Book const& book, SessionPtrType session); + unsubTransactions(SubscriberSharedPtr const& subscriber); /** - * @brief Unsubscribe from the specific book changes stream. - * - * @param book The book to stop monitoring changes for - * @param session The session to unsubscribe from the stream + * @brief Subscribe to the transactions feed, only receive the feed when particular account is affected. + * @param account The account to watch. + * @param subscriber + * @param apiVersion The api version of feed to subscribe. */ void - unsubBook(ripple::Book const& book, SessionPtrType session); + subAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); /** - * @brief Subscribe to the book changes stream. - * - * @param session The session to subscribe to the stream + * @brief Unsubscribe to the transactions feed for particular account. + * @param subscriber */ void - subBookChanges(SessionPtrType session); + unsubAccount(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); /** - * @brief Unsubscribe from the book changes stream. - * - * @param session The session to unsubscribe from the stream + * @brief Subscribe to the transactions feed, only receive feed when particular order book is affected. + * @param book The book to watch. + * @param subscriber + * @param apiVersion The api version of feed to subscribe. */ void - unsubBookChanges(SessionPtrType session); + subBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); /** - * @brief Subscribe to the manifest stream. - * - * @param session The session to subscribe to the stream + * @brief Unsubscribe to the transactions feed for particular order book. + * @param book The book to watch. + * @param subscriber */ void - subManifest(SessionPtrType session); + unsubBook(ripple::Book const& book, SubscriberSharedPtr const& subscriber); /** - * @brief Unsubscribe from the manifest stream. - * - * @param session The session to unsubscribe from the stream + * @brief Forward the transactions feed. + * @param txMeta The transaction and metadata. + * @param lgrInfo The ledger header. */ void - unsubManifest(SessionPtrType session); + pubTransaction(data::TransactionAndMetadata const& txMeta, ripple::LedgerHeader const& lgrInfo); /** - * @brief Subscribe to the validation stream. - * - * @param session The session to subscribe to the stream - */ - void - subValidation(SessionPtrType session); - - /** - * @brief Unsubscribe from the validation stream. - * - * @param session The session to unsubscribe from the stream - */ - void - unsubValidation(SessionPtrType session); - - /** - * @brief Publish proposed transactions and proposed accounts from a JSON response. - * - * @param response The JSON response to use - */ - void - forwardProposedTransaction(boost::json::object const& response); - - /** - * @brief Publish manifest updates from a JSON response. - * - * @param response The JSON response to use - */ - void - forwardManifest(boost::json::object const& response); - - /** - * @brief Publish validation updates from a JSON response. - * - * @param response The JSON response to use - */ - void - forwardValidation(boost::json::object const& response); - - /** - * @brief Subscribe to the proposed account stream. - * - * @param account The account to monitor - * @param session The session to subscribe to the stream - */ - void - subProposedAccount(ripple::AccountID const& account, SessionPtrType session); - - /** - * @brief Unsubscribe from the proposed account stream. - * - * @param account The account the stream is for - * @param session The session to unsubscribe from the stream - */ - void - unsubProposedAccount(ripple::AccountID const& account, SessionPtrType session); - - /** - * @brief Subscribe to the processed transactions stream. - * - * @param session The session to subscribe to the stream - */ - void - subProposedTransactions(SessionPtrType session); - - /** - * @brief Unsubscribe from the proposed transactions stream. - * - * @param session The session to unsubscribe from the stream - */ - void - unsubProposedTransactions(SessionPtrType session); - - /** @brief Clenup the session on removal. */ - void - cleanup(SessionPtrType session); - - /** - * @brief Generate a JSON report on the current state of the subscriptions. - * - * @return The report as a JSON object + * @brief Get the number of subscribers. */ boost::json::object report() const { return { - {"ledger", ledgerSubscribers_.count()}, - {"transactions", txSubscribers_.count()}, - {"transactions_proposed", txProposedSubscribers_.count()}, - {"manifests", manifestSubscribers_.count()}, - {"validations", validationsSubscribers_.count()}, - {"account", accountSubscribers_.count()}, - {"accounts_proposed", accountProposedSubscribers_.count()}, - {"books", bookSubscribers_.count()}, - {"book_changes", bookChangesSubscribers_.count()}, + {"ledger", ledgerFeed_.count()}, + {"transactions", transactionFeed_.transactionSubCount()}, + {"transactions_proposed", proposedTransactionFeed_.transactionSubcount()}, + {"manifests", manifestFeed_.count()}, + {"validations", validationsFeed_.count()}, + {"account", transactionFeed_.accountSubCount()}, + {"accounts_proposed", proposedTransactionFeed_.accountSubCount()}, + {"books", transactionFeed_.bookSubCount()}, + {"book_changes", bookChangesFeed_.count()}, }; } - -private: - using CleanupFunction = std::function; - - void - subscribeHelper(SessionPtrType const& session, Subscription& subs, CleanupFunction&& func); - - template - void - subscribeHelper(SessionPtrType const& session, Key const& k, SubscriptionMap& subs, CleanupFunction&& func); - - // This is how we chose to cleanup subscriptions that have been closed. - // Each time we add a subscriber, we add the opposite lambda that unsubscribes that subscriber when cleanup is - // called with the session that closed. - std::mutex cleanupMtx_; - std::unordered_map> cleanupFuncs_ = {}; }; +/** + * @brief The help class to run the subscription manager. The container of io_context which is used to publish the + * feeds. + */ +class SubscriptionManagerRunner { + boost::asio::io_context ioContext_; + std::shared_ptr subscriptionManager_; + util::Logger logger_{"Subscriptions"}; + boost::asio::executor_work_guard work_ = + boost::asio::make_work_guard(ioContext_); + std::vector workers_; + +public: + SubscriptionManagerRunner(util::Config const& config, std::shared_ptr const& backend) + : subscriptionManager_(std::make_shared(ioContext_, backend)) + { + auto numThreads = config.valueOr("subscription_workers", 1); + LOG(logger_.info()) << "Starting subscription manager with " << numThreads << " workers"; + workers_.reserve(numThreads); + for (auto i = numThreads; i > 0; --i) + workers_.emplace_back([&] { ioContext_.run(); }); + } + + std::shared_ptr + getManager() + { + return subscriptionManager_; + } + + ~SubscriptionManagerRunner() + { + work_.reset(); + for (auto& worker : workers_) + worker.join(); + } +}; } // namespace feed diff --git a/src/feed/Types.h b/src/feed/Types.h new file mode 100644 index 00000000..9b081148 --- /dev/null +++ b/src/feed/Types.h @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +/* + 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 "web/interface/ConnectionBase.h" + +#include + +namespace feed { +using Subscriber = web::ConnectionBase; +using SubscriberPtr = Subscriber*; +using SubscriberSharedPtr = std::shared_ptr; + +} // namespace feed diff --git a/src/feed/impl/BookChangesFeed.h b/src/feed/impl/BookChangesFeed.h new file mode 100644 index 00000000..0a01b9e7 --- /dev/null +++ b/src/feed/impl/BookChangesFeed.h @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "data/Types.h" +#include "feed/impl/SingleFeedBase.h" +#include "rpc/BookChangesHelper.h" + +#include +#include +#include + +#include + +namespace feed::impl { + +/** + * @brief Feed that publishes book changes. This feed will be published every ledger, even if there are no changes. + * Example : {'type': 'bookChanges', 'ledger_index': 2647936, 'ledger_hash': + * '0A5010342D8AAFABDCA58A68F6F588E1C6E58C21B63ED6CA8DB2478F58F3ECD5', 'ledger_time': 756395682, 'changes': []} + */ +struct BookChangesFeed : public SingleFeedBase { + BookChangesFeed(boost::asio::io_context& ioContext) : SingleFeedBase(ioContext, "book_changes") + { + } + + /** + * @brief Publishes the book changes. + * @param lgrInfo The ledger header. + * @param transactions The transactions that were included in the ledger. + */ + void + pub(ripple::LedgerHeader const& lgrInfo, std::vector const& transactions) const + { + SingleFeedBase::pub(boost::json::serialize(rpc::computeBookChanges(lgrInfo, transactions))); + } +}; +} // namespace feed::impl diff --git a/src/feed/impl/ForwardFeed.h b/src/feed/impl/ForwardFeed.h new file mode 100644 index 00000000..50c603dd --- /dev/null +++ b/src/feed/impl/ForwardFeed.h @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/SingleFeedBase.h" + +#include +#include + +namespace feed::impl { + +/** + * @brief Feed that publishes the json object as it is. + */ +struct ForwardFeed : public SingleFeedBase { + using SingleFeedBase::SingleFeedBase; + + /** + * @brief Publishes the json object. + */ + void + pub(boost::json::object const& json) const + { + SingleFeedBase::pub(boost::json::serialize(json)); + } +}; +} // namespace feed::impl diff --git a/src/feed/impl/LedgerFeed.cpp b/src/feed/impl/LedgerFeed.cpp new file mode 100644 index 00000000..fee2d75f --- /dev/null +++ b/src/feed/impl/LedgerFeed.cpp @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/LedgerFeed.h" + +#include "data/BackendInterface.h" +#include "feed/Types.h" +#include "feed/impl/SingleFeedBase.h" +#include "rpc/RPCHelpers.h" +#include "util/Assert.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace feed::impl { + +boost::json::object +LedgerFeed::makeLedgerPubMessage( + ripple::LedgerHeader const& lgrInfo, + ripple::Fees const& fees, + std::string const& ledgerRange, + std::uint32_t const txnCount +) +{ + boost::json::object pubMsg; + pubMsg["type"] = "ledgerClosed"; + pubMsg["ledger_index"] = lgrInfo.seq; + pubMsg["ledger_hash"] = to_string(lgrInfo.hash); + pubMsg["ledger_time"] = lgrInfo.closeTime.time_since_epoch().count(); + pubMsg["fee_base"] = rpc::toBoostJson(fees.base.jsonClipped()); + pubMsg["reserve_base"] = rpc::toBoostJson(fees.reserve.jsonClipped()); + pubMsg["reserve_inc"] = rpc::toBoostJson(fees.increment.jsonClipped()); + pubMsg["validated_ledgers"] = ledgerRange; + pubMsg["txn_count"] = txnCount; + return pubMsg; +} + +boost::json::object +LedgerFeed::sub( + boost::asio::yield_context yield, + std::shared_ptr const& backend, + SubscriberSharedPtr const& subscriber +) +{ + SingleFeedBase::sub(subscriber); + + // For ledger stream, we need to send the last closed ledger info as response + auto const ledgerRange = backend->fetchLedgerRange(); + ASSERT(ledgerRange.has_value(), "Ledger range must be valid"); + + auto const lgrInfo = backend->fetchLedgerBySequence(ledgerRange->maxSequence, yield); + ASSERT(lgrInfo.has_value(), "Ledger must be valid"); + + auto const fees = backend->fetchFees(lgrInfo->seq, yield); + ASSERT(fees.has_value(), "Fees must be valid"); + + auto const range = std::to_string(ledgerRange->minSequence) + "-" + std::to_string(ledgerRange->maxSequence); + + auto pubMsg = makeLedgerPubMessage(*lgrInfo, *fees, range, 0); + pubMsg.erase("txn_count"); + pubMsg.erase("type"); + + return pubMsg; +} + +void +LedgerFeed::pub( + ripple::LedgerHeader const& lgrInfo, + ripple::Fees const& fees, + std::string const& ledgerRange, + std::uint32_t const txnCount +) const +{ + SingleFeedBase::pub(boost::json::serialize(makeLedgerPubMessage(lgrInfo, fees, ledgerRange, txnCount))); +} +} // namespace feed::impl diff --git a/src/feed/impl/LedgerFeed.h b/src/feed/impl/LedgerFeed.h new file mode 100644 index 00000000..685284b2 --- /dev/null +++ b/src/feed/impl/LedgerFeed.h @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +/* + 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.h" +#include "feed/Types.h" +#include "feed/impl/SingleFeedBase.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace feed::impl { + +/** + * @brief Feed that publishes the ledger info. + * Example : {'type': 'ledgerClosed', 'ledger_index': 2647935, 'ledger_hash': + * '5D022718CD782A82EE10D2147FD90B5F42F26A7E937C870B4FE3CF1086C916AE', 'ledger_time': 756395681, 'fee_base': 10, + * 'reserve_base': 10000000, 'reserve_inc': 2000000, 'validated_ledgers': '2619127-2647935', 'txn_count': 0} + */ +class LedgerFeed : public SingleFeedBase { +public: + /** + * @brief Construct a new Ledger Feed object + * @param ioContext The actual publish will be called in the strand of this. + */ + LedgerFeed(boost::asio::io_context& ioContext) : SingleFeedBase(ioContext, "ledger") + { + } + + /** + * @brief Subscribe the ledger feed. + * @param yield The coroutine yield. + * @param backend The backend. + * @param subscriber + * @return The information of the latest ledger. + */ + boost::json::object + sub(boost::asio::yield_context yield, + std::shared_ptr const& backend, + SubscriberSharedPtr const& subscriber); + + /** + * @brief Publishes the ledger feed. + * @param lgrInfo The ledger header. + * @param fees The fees. + * @param ledgerRange The ledger range. + * @param txnCount The transaction count. + */ + void + pub(ripple::LedgerHeader const& lgrInfo, + ripple::Fees const& fees, + std::string const& ledgerRange, + std::uint32_t txnCount) const; + +private: + static boost::json::object + makeLedgerPubMessage( + ripple::LedgerHeader const& lgrInfo, + ripple::Fees const& fees, + std::string const& ledgerRange, + std::uint32_t txnCount + ); +}; +} // namespace feed::impl diff --git a/src/feed/impl/ProposedTransactionFeed.cpp b/src/feed/impl/ProposedTransactionFeed.cpp new file mode 100644 index 00000000..e57f3112 --- /dev/null +++ b/src/feed/impl/ProposedTransactionFeed.cpp @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/ProposedTransactionFeed.h" + +#include "feed/Types.h" +#include "rpc/RPCHelpers.h" +#include "util/log/Logger.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace feed::impl { + +void +ProposedTransactionFeed::sub(SubscriberSharedPtr const& subscriber) +{ + auto const weakPtr = std::weak_ptr(subscriber); + auto const added = signal_.connectTrackableSlot(subscriber, [weakPtr](std::shared_ptr const& msg) { + if (auto connectionPtr = weakPtr.lock()) { + connectionPtr->send(msg); + } + }); + + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed tx_proposed"; + ++subAllCount_.get(); + subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); }); + } +} + +void +ProposedTransactionFeed::sub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) +{ + auto const weakPtr = std::weak_ptr(subscriber); + auto const added = accountSignal_.connectTrackableSlot( + subscriber, + account, + [this, weakPtr](std::shared_ptr const& msg) { + if (auto connectionPtr = weakPtr.lock()) { + // Check if this connection already sent + if (notified_.contains(connectionPtr.get())) + return; + + notified_.insert(connectionPtr.get()); + connectionPtr->send(msg); + } + } + ); + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed accounts_proposed " << account; + ++subAccountCount_.get(); + subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) { + unsubInternal(account, connection); + }); + } +} + +void +ProposedTransactionFeed::unsub(SubscriberSharedPtr const& subscriber) +{ + unsubInternal(subscriber.get()); +} + +void +ProposedTransactionFeed::unsub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) +{ + unsubInternal(account, subscriber.get()); +} + +void +ProposedTransactionFeed::pub(boost::json::object const& receivedTxJson) +{ + auto pubMsg = std::make_shared(boost::json::serialize(receivedTxJson)); + + auto const transaction = receivedTxJson.at("transaction").as_object(); + auto const accounts = rpc::getAccountsFromTransaction(transaction); + auto affectedAccounts = std::unordered_set(accounts.cbegin(), accounts.cend()); + + boost::asio::post(strand_, [this, pubMsg = std::move(pubMsg), affectedAccounts = std::move(affectedAccounts)]() { + signal_.emit(pubMsg); + // Prevent the same connection from receiving the same message twice if it is subscribed to multiple accounts + // However, if the same connection subscribe both stream and account, it will still receive the message twice. + // notified_ can be cleared before signal_ emit to improve this, but let's keep it as is for now, since rippled + // acts like this. + notified_.clear(); + for (auto const& account : affectedAccounts) + accountSignal_.emit(account, pubMsg); + }); +} + +std::uint64_t +ProposedTransactionFeed::transactionSubcount() const +{ + return subAllCount_.get().value(); +} + +std::uint64_t +ProposedTransactionFeed::accountSubCount() const +{ + return subAccountCount_.get().value(); +} + +void +ProposedTransactionFeed::unsubInternal(SubscriberPtr subscriber) +{ + if (signal_.disconnect(subscriber)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed tx_proposed"; + --subAllCount_.get(); + } +} + +void +ProposedTransactionFeed::unsubInternal(ripple::AccountID const& account, SubscriberPtr subscriber) +{ + if (accountSignal_.disconnect(subscriber, account)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed accounts_proposed " << account; + --subAccountCount_.get(); + } +} + +} // namespace feed::impl diff --git a/src/feed/impl/ProposedTransactionFeed.h b/src/feed/impl/ProposedTransactionFeed.h new file mode 100644 index 00000000..47eea197 --- /dev/null +++ b/src/feed/impl/ProposedTransactionFeed.h @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/Types.h" +#include "feed/impl/TrackableSignal.h" +#include "feed/impl/TrackableSignalMap.h" +#include "feed/impl/Util.h" +#include "util/log/Logger.h" +#include "util/prometheus/Gauge.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace feed::impl { + +/** + * @brief Feed that publishes the Proposed Transactions. + * @note Be aware that the Clio only forwards this stream, not respect api_version. + */ +class ProposedTransactionFeed { + util::Logger logger_{"Subscriptions"}; + + std::unordered_set + notified_; // Used by slots to prevent double notifications if tx contains multiple subscribed accounts + boost::asio::strand strand_; + std::reference_wrapper subAllCount_; + std::reference_wrapper subAccountCount_; + + TrackableSignalMap> accountSignal_; + TrackableSignal> signal_; + +public: + /** + * @brief Construct a Proposed Transaction Feed object. + * @param ioContext The actual publish will be called in the strand of this. + */ + ProposedTransactionFeed(boost::asio::io_context& ioContext) + : strand_(boost::asio::make_strand(ioContext)) + , subAllCount_(getSubscriptionsGaugeInt("tx_proposed")) + , subAccountCount_(getSubscriptionsGaugeInt("account_proposed")) + + { + } + + /** + * @brief Subscribe to the proposed transaction feed. + * @param subscriber + */ + void + sub(SubscriberSharedPtr const& subscriber); + + /** + * @brief Subscribe to the proposed transaction feed, only receive the feed when particular account is affected. + * @param subscriber + * @param account The account to watch. + */ + void + sub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the proposed transaction feed. + * @param subscriber + */ + void + unsub(SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the proposed transaction feed for particular account. + * @param subscriber + * @param account The account to unsubscribe. + */ + void + unsub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); + + /** + * @brief Publishes the proposed transaction feed. + * @param receivedTxJson The proposed transaction json. + */ + void + pub(boost::json::object const& receivedTxJson); + + /** + * @brief Get the number of subscribers of the proposed transaction feed. + */ + std::uint64_t + transactionSubcount() const; + + /** + * @brief Get the number of accounts subscribers. + */ + std::uint64_t + accountSubCount() const; + +private: + void + unsubInternal(SubscriberPtr subscriber); + + void + unsubInternal(ripple::AccountID const& account, SubscriberPtr subscriber); +}; +} // namespace feed::impl diff --git a/src/feed/impl/SingleFeedBase.cpp b/src/feed/impl/SingleFeedBase.cpp new file mode 100644 index 00000000..c4a36cd0 --- /dev/null +++ b/src/feed/impl/SingleFeedBase.cpp @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/SingleFeedBase.h" + +#include "feed/Types.h" +#include "feed/impl/TrackableSignal.h" +#include "feed/impl/Util.h" +#include "util/log/Logger.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace feed::impl { + +SingleFeedBase::SingleFeedBase(boost::asio::io_context& ioContext, std::string const& name) + : strand_(boost::asio::make_strand(ioContext)), subCount_(getSubscriptionsGaugeInt(name)), name_(name) +{ +} + +void +SingleFeedBase::sub(SubscriberSharedPtr const& subscriber) +{ + auto const weakPtr = std::weak_ptr(subscriber); + auto const added = signal_.connectTrackableSlot(subscriber, [weakPtr](std::shared_ptr const& msg) { + if (auto connectionPtr = weakPtr.lock()) + connectionPtr->send(msg); + }); + + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed " << name_; + ++subCount_.get(); + subscriber->onDisconnect.connect([this](SubscriberPtr connectionDisconnecting) { + unsubInternal(connectionDisconnecting); + }); + }; +} + +void +SingleFeedBase::unsub(SubscriberSharedPtr const& subscriber) +{ + unsubInternal(subscriber.get()); +} + +void +SingleFeedBase::pub(std::string msg) const +{ + boost::asio::post(strand_, [this, msg = std::move(msg)]() mutable { + auto const msgPtr = std::make_shared(std::move(msg)); + signal_.emit(msgPtr); + }); +} + +std::uint64_t +SingleFeedBase::count() const +{ + return subCount_.get().value(); +} + +void +SingleFeedBase::unsubInternal(SubscriberPtr subscriber) +{ + if (signal_.disconnect(subscriber)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed " << name_; + --subCount_.get(); + } +} +} // namespace feed::impl diff --git a/src/feed/impl/SingleFeedBase.h b/src/feed/impl/SingleFeedBase.h new file mode 100644 index 00000000..6f5b0cbc --- /dev/null +++ b/src/feed/impl/SingleFeedBase.h @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/Types.h" +#include "feed/impl/TrackableSignal.h" +#include "util/log/Logger.h" +#include "util/prometheus/Gauge.h" + +#include +#include + +#include +#include +#include +#include + +namespace feed::impl { + +/** + * @brief Base class for single feed. + */ +class SingleFeedBase { + boost::asio::strand strand_; + std::reference_wrapper subCount_; + TrackableSignal const&> signal_; + util::Logger logger_{"Subscriptions"}; + std::string name_; + +public: + /** + * @brief Construct a new Single Feed Base object + * @param ioContext The actual publish will be called in the strand of this. + * @param name The promethues counter name of the feed. + */ + SingleFeedBase(boost::asio::io_context& ioContext, std::string const& name); + + /** + * @brief Subscribe the feed. + * @param subscriber + */ + void + sub(SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe the feed. + * @param subscriber + */ + void + unsub(SubscriberSharedPtr const& subscriber); + + /** + * @brief Publishes the feed in strand. + * @param msg The message. + */ + void + pub(std::string msg) const; + + /** + * @brief Get the count of subscribers. + */ + std::uint64_t + count() const; + +private: + void + unsubInternal(SubscriberPtr subscriber); +}; +} // namespace feed::impl diff --git a/src/feed/impl/TrackableSignal.h b/src/feed/impl/TrackableSignal.h new file mode 100644 index 00000000..0384d7de --- /dev/null +++ b/src/feed/impl/TrackableSignal.h @@ -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 +#include + +#include +#include +#include +#include +#include + +namespace feed::impl { + +/** + * @brief A thread-safe class to manage a signal and its tracking connections. + * + * @param Session The type of the object that will be tracked, when the object is destroyed, the connection will be + * removed lazily. The pointer of the session object will also be the key to disconnect. + * @param Args The types of the arguments that will be passed to the slot. + */ +template +class TrackableSignal { + using ConnectionPtr = Session*; + using ConnectionSharedPtr = std::shared_ptr; + + // map of connection and signal connection, key is the pointer of the connection object + // allow disconnect to be called in the destructor of the connection + std::unordered_map connections_; + mutable std::mutex mutex_; + + using SignalType = boost::signals2::signal; + SignalType signal_; + +public: + /** + * @brief Connect a slot to the signal, the slot will be called when the signal is emitted and trackable is still + * alive. + * + * @param trackable Track this object's lifttime, if the object is destroyed, the connection will be removed lazily. + * When the slot is being called, the object is guaranteed to be alive. + * @param slot The slot connecting to the signal, the slot will be called when the signal is emitted. + * @return true if the connection is successfully added, false if the connection already exists. + */ + bool + connectTrackableSlot(ConnectionSharedPtr const& trackable, std::function slot) + { + std::scoped_lock const lk(mutex_); + if (connections_.contains(trackable.get())) { + return false; + } + + // This class can't hold the trackable's shared_ptr, because disconnect should be able to be called in the + // the trackable's destructor. However, the trackable can not be destroied when the slot is being called + // either. track_foreign will hold a weak_ptr to the connection, which makes sure the connection is valid when + // the slot is called. + connections_.emplace( + trackable.get(), signal_.connect(typename SignalType::slot_type(slot).track_foreign(trackable)) + ); + return true; + } + + /** + * @brief Disconnect a slot to the signal. + * + * @param trackablePtr Disconnect the slot whose trackable is this pointer. Be aware that the pointer is a raw + * pointer, allowing disconnect to be called in the destructor of the trackable. + * @return true if the connection is successfully disconnected, false if the connection does not exist. + */ + bool + disconnect(ConnectionPtr trackablePtr) + { + std::scoped_lock const lk(mutex_); + if (connections_.contains(trackablePtr)) { + connections_[trackablePtr].disconnect(); + connections_.erase(trackablePtr); + return true; + } + return false; + } + + /** + * @brief Calling all slots. + * + * @param args The arguments to pass to the slots. + */ + void + emit(Args const&... args) const + { + signal_(args...); + } + + /** + * @brief Get the number of connections. + */ + std::size_t + count() const + { + std::scoped_lock const lk(mutex_); + return connections_.size(); + } +}; +} // namespace feed::impl diff --git a/src/feed/impl/TrackableSignalMap.h b/src/feed/impl/TrackableSignalMap.h new file mode 100644 index 00000000..3146669b --- /dev/null +++ b/src/feed/impl/TrackableSignalMap.h @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/TrackableSignal.h" + +#include + +#include +#include +#include +#include +#include + +namespace feed::impl { + +template +concept Hashable = requires(T a) { + { + std::hash{}(a) + } -> std::convertible_to; +}; + +/** + * @brief Class to manage a map of key and its associative signal. + * @param Key The type of the key. + * @param Session The type of the object that will be tracked, when the object is destroyed, the connection will be + * removed lazily. + * @param Args The types of the arguments that will be passed to the slot + */ +template +class TrackableSignalMap { + using ConnectionPtr = Session*; + using ConnectionSharedPtr = std::shared_ptr; + + mutable std::mutex mutex_; + std::unordered_map> signalsMap_; + +public: + /** + * @brief Connect a slot to the signal, the slot will be called when the signal is emitted and trackable is still + * alive. + * + * @param trackable Track this object's lifttime, if the object is destroyed, the connection will be removed lazily. + * When the slot is being called, the object is guaranteed to be alive. + * @param key The key to the signal. + * @param slot The slot connecting to the signal, the slot will be called when the assocaiative signal is emitted. + * @return true if the connection is successfully added, false if the connection already exists for the key. + */ + bool + connectTrackableSlot(ConnectionSharedPtr const& trackable, Key const& key, std::function slot) + { + std::scoped_lock const lk(mutex_); + return signalsMap_[key].connectTrackableSlot(trackable, slot); + } + + /** + * @brief Disconnect a slot from the key's associative signal. + * + * @param trackablePtr The pointer to the object that is being tracked. + * @param key The key to the signal. + * @return true if the connection is successfully removed, false if the connection does not exist. + */ + bool + disconnect(ConnectionPtr trackablePtr, Key const& key) + { + std::scoped_lock const lk(mutex_); + if (!signalsMap_.contains(key)) + return false; + + auto const disconnected = signalsMap_[key].disconnect(trackablePtr); + // clean the map if there is no connection left. + if (disconnected && signalsMap_[key].count() == 0) + signalsMap_.erase(key); + + return disconnected; + } + + /** + * @brief Emit the signal with the given key and arguments. + * + * @param key The key to the signal. + * @param args The arguments to be passed to the slot. + */ + void + emit(Key const& key, Args const&... args) + { + std::scoped_lock const lk(mutex_); + if (signalsMap_.contains(key)) + signalsMap_[key].emit(args...); + } +}; +} // namespace feed::impl diff --git a/src/feed/impl/TransactionFeed.cpp b/src/feed/impl/TransactionFeed.cpp new file mode 100644 index 00000000..c3756605 --- /dev/null +++ b/src/feed/impl/TransactionFeed.cpp @@ -0,0 +1,303 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "feed/impl/TransactionFeed.h" + +#include "data/BackendInterface.h" +#include "data/Types.h" +#include "feed/Types.h" +#include "rpc/JS.h" +#include "rpc/RPCHelpers.h" +#include "util/log/Logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace feed::impl { + +void +TransactionFeed::TransactionSlot::operator()(AllVersionTransactionsType const& allVersionMsgs) const +{ + if (auto connection = connectionWeakPtr.lock(); connection) { + // Check if this connection already sent + if (feed.get().notified_.contains(connection.get())) + return; + + feed.get().notified_.insert(connection.get()); + + if (connection->apiSubVersion < 2u) { + connection->send(allVersionMsgs[0]); + return; + } + connection->send(allVersionMsgs[1]); + } +} + +void +TransactionFeed::sub(SubscriberSharedPtr const& subscriber, std::uint32_t const apiVersion) +{ + auto const added = signal_.connectTrackableSlot(subscriber, TransactionSlot(*this, subscriber)); + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed transactions"; + ++subAllCount_.get(); + subscriber->apiSubVersion = apiVersion; + subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); }); + } +} + +void +TransactionFeed::sub( + ripple::AccountID const& account, + SubscriberSharedPtr const& subscriber, + std::uint32_t const apiVersion +) +{ + auto const added = accountSignal_.connectTrackableSlot(subscriber, account, TransactionSlot(*this, subscriber)); + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed account " << account; + ++subAccountCount_.get(); + subscriber->apiSubVersion = apiVersion; + subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) { + unsubInternal(account, connection); + }); + } +} + +void +TransactionFeed::sub(ripple::Book const& book, SubscriberSharedPtr const& subscriber, std::uint32_t const apiVersion) +{ + auto const added = bookSignal_.connectTrackableSlot(subscriber, book, TransactionSlot(*this, subscriber)); + if (added) { + LOG(logger_.debug()) << subscriber->tag() << "Subscribed book " << book; + ++subBookCount_.get(); + subscriber->apiSubVersion = apiVersion; + subscriber->onDisconnect.connect([this, book](SubscriberPtr connection) { unsubInternal(book, connection); }); + } +} + +void +TransactionFeed::unsub(SubscriberSharedPtr const& subscriber) +{ + unsubInternal(subscriber.get()); +} + +void +TransactionFeed::unsub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber) +{ + unsubInternal(account, subscriber.get()); +} + +void +TransactionFeed::unsub(ripple::Book const& book, SubscriberSharedPtr const& subscriber) +{ + unsubInternal(book, subscriber.get()); +} + +std::uint64_t +TransactionFeed::transactionSubCount() const +{ + return subAllCount_.get().value(); +} + +std::uint64_t +TransactionFeed::accountSubCount() const +{ + return subAccountCount_.get().value(); +} + +std::uint64_t +TransactionFeed::bookSubCount() const +{ + return subBookCount_.get().value(); +} + +void +TransactionFeed::pub( + data::TransactionAndMetadata const& txMeta, + ripple::LedgerHeader const& lgrInfo, + std::shared_ptr const& backend +) +{ + auto [tx, meta] = rpc::deserializeTxPlusMeta(txMeta, lgrInfo.seq); + + std::optional ownerFunds; + + if (tx->getTxnType() == ripple::ttOFFER_CREATE) { + auto const account = tx->getAccountID(ripple::sfAccount); + auto const amount = tx->getFieldAmount(ripple::sfTakerGets); + if (account != amount.issue().account) { + auto fetchFundsSynchronous = [&]() { + data::synchronous([&](boost::asio::yield_context yield) { + ownerFunds = rpc::accountFunds(*backend, lgrInfo.seq, amount, account, yield); + }); + }; + data::retryOnTimeout(fetchFundsSynchronous); + } + } + + auto const genJsonByVersion = [&, tx = tx, meta = meta](std::uint32_t version) { + boost::json::object pubObj; + auto const txKey = version < 2u ? JS(transaction) : JS(tx_json); + pubObj[txKey] = rpc::toJson(*tx); + pubObj[JS(meta)] = rpc::toJson(*meta); + rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date); + rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version); + + pubObj[JS(type)] = "transaction"; + pubObj[JS(validated)] = true; + pubObj[JS(status)] = "closed"; + pubObj[JS(close_time_iso)] = ripple::to_string_iso(lgrInfo.closeTime); + + pubObj[JS(ledger_index)] = lgrInfo.seq; + pubObj[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); + if (version >= 2u) { + if (pubObj[txKey].as_object().contains(JS(hash))) { + pubObj[JS(hash)] = pubObj[txKey].as_object()[JS(hash)]; + pubObj[txKey].as_object().erase(JS(hash)); + } + } + pubObj[txKey].as_object()[JS(date)] = lgrInfo.closeTime.time_since_epoch().count(); + + pubObj[JS(engine_result_code)] = meta->getResult(); + std::string token; + std::string human; + ripple::transResultInfo(meta->getResultTER(), token, human); + pubObj[JS(engine_result)] = token; + pubObj[JS(engine_result_message)] = human; + + if (ownerFunds) + pubObj[txKey].as_object()[JS(owner_funds)] = ownerFunds->getText(); + + return pubObj; + }; + + AllVersionTransactionsType allVersionsMsgs{ + std::make_shared(boost::json::serialize(genJsonByVersion(1u))), + std::make_shared(boost::json::serialize(genJsonByVersion(2u))) + }; + + auto const affectedAccountsFlat = meta->getAffectedAccounts(); + auto affectedAccounts = + std::unordered_set(affectedAccountsFlat.cbegin(), affectedAccountsFlat.cend()); + + std::unordered_set affectedBooks; + + for (auto const& node : meta->getNodes()) { + if (node.getFieldU16(ripple::sfLedgerEntryType) == ripple::ltOFFER) { + ripple::SField const* field = nullptr; + + // We need a field that contains the TakerGets and TakerPays + // parameters. + if (node.getFName() == ripple::sfModifiedNode) { + field = &ripple::sfPreviousFields; + } else if (node.getFName() == ripple::sfCreatedNode) { + field = &ripple::sfNewFields; + } else if (node.getFName() == ripple::sfDeletedNode) { + field = &ripple::sfFinalFields; + } + + if (field != nullptr) { + auto const data = dynamic_cast(node.peekAtPField(*field)); + + if ((data != nullptr) && data->isFieldPresent(ripple::sfTakerPays) && + data->isFieldPresent(ripple::sfTakerGets)) { + // determine the OrderBook + ripple::Book const book{ + data->getFieldAmount(ripple::sfTakerGets).issue(), + data->getFieldAmount(ripple::sfTakerPays).issue() + }; + if (affectedBooks.find(book) == affectedBooks.end()) { + affectedBooks.insert(book); + } + } + } + } + } + + boost::asio::post( + strand_, + [this, + allVersionsMsgs = std::move(allVersionsMsgs), + affectedAccounts = std::move(affectedAccounts), + affectedBooks = std::move(affectedBooks)]() { + notified_.clear(); + signal_.emit(allVersionsMsgs); + notified_.clear(); + // check duplicate for accounts, this prevents sending the same message multiple times if it touches + // multiple accounts watched by the same connection + for (auto const& account : affectedAccounts) { + accountSignal_.emit(account, allVersionsMsgs); + } + notified_.clear(); + // check duplicate for books, this prevents sending the same message multiple times if it touches multiple + // books watched by the same connection + for (auto const& book : affectedBooks) { + bookSignal_.emit(book, allVersionsMsgs); + } + } + ); +} + +void +TransactionFeed::unsubInternal(SubscriberPtr subscriber) +{ + if (signal_.disconnect(subscriber)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed transactions"; + --subAllCount_.get(); + } +} + +void +TransactionFeed::unsubInternal(ripple::AccountID const& account, SubscriberPtr subscriber) +{ + if (accountSignal_.disconnect(subscriber, account)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed account " << account; + --subAccountCount_.get(); + } +} + +void +TransactionFeed::unsubInternal(ripple::Book const& book, SubscriberPtr subscriber) +{ + if (bookSignal_.disconnect(subscriber, book)) { + LOG(logger_.debug()) << subscriber->tag() << "Unsubscribed book " << book; + --subBookCount_.get(); + } +} +} // namespace feed::impl diff --git a/src/feed/impl/TransactionFeed.h b/src/feed/impl/TransactionFeed.h new file mode 100644 index 00000000..20685c1e --- /dev/null +++ b/src/feed/impl/TransactionFeed.h @@ -0,0 +1,179 @@ +//------------------------------------------------------------------------------ +/* + 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.h" +#include "data/Types.h" +#include "feed/Types.h" +#include "feed/impl/TrackableSignal.h" +#include "feed/impl/TrackableSignalMap.h" +#include "feed/impl/Util.h" +#include "util/log/Logger.h" +#include "util/prometheus/Gauge.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace feed::impl { + +class TransactionFeed { + // Hold two versions of transaction messages + using AllVersionTransactionsType = std::array, 2>; + + struct TransactionSlot { + std::reference_wrapper feed; + std::weak_ptr connectionWeakPtr; + + TransactionSlot(TransactionFeed& feed, SubscriberSharedPtr const& connection) + : feed(feed), connectionWeakPtr(connection) + { + } + + void + operator()(AllVersionTransactionsType const& allVersionMsgs) const; + }; + + util::Logger logger_{"Subscriptions"}; + + boost::asio::strand strand_; + std::reference_wrapper subAllCount_; + std::reference_wrapper subAccountCount_; + std::reference_wrapper subBookCount_; + + TrackableSignalMap accountSignal_; + TrackableSignalMap bookSignal_; + TrackableSignal signal_; + + std::unordered_set + notified_; // Used by slots to prevent double notifications if tx contains multiple subscribed accounts + +public: + /** + * @brief Construct a new Transaction Feed object. + * @param ioContext The actual publish will be called in the strand of this. + */ + TransactionFeed(boost::asio::io_context& ioContext) + : strand_(boost::asio::make_strand(ioContext)) + , subAllCount_(getSubscriptionsGaugeInt("tx")) + , subAccountCount_(getSubscriptionsGaugeInt("account")) + , subBookCount_(getSubscriptionsGaugeInt("book")) + { + } + + /** + * @brief Subscribe to the transaction feed. + * @param subscriber + * @param apiVersion The api version of feed. + */ + void + sub(SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); + + /** + * @brief Subscribe to the transaction feed, only receive the feed when particular account is affected. + * @param subscriber + * @param account The account to watch. + * @param apiVersion The api version of feed. + */ + void + sub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); + + /** + * @brief Subscribe to the transaction feed, only receive the feed when particular order book is affected. + * @param subscriber + * @param book The order book to watch. + * @param apiVersion The api version of feed. + */ + void + sub(ripple::Book const& book, SubscriberSharedPtr const& subscriber, std::uint32_t apiVersion); + + /** + * @brief Unsubscribe to the transaction feed. + * @param subscriber + */ + void + unsub(SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the transaction for particular account. + * @param subscriber + * @param account The account to unsubscribe. + */ + void + unsub(ripple::AccountID const& account, SubscriberSharedPtr const& subscriber); + + /** + * @brief Unsubscribe to the transaction feed for particular order book. + * @param subscriber + * @param book The book to unsubscribe. + */ + void + unsub(ripple::Book const& book, SubscriberSharedPtr const& subscriber); + + /** + * @brief Publishes the transaction feed. + * @param txMeta The transaction and metadata. + * @param lgrInfo The ledger header. + * @param backend The backend. + */ + void + pub(data::TransactionAndMetadata const& txMeta, + ripple::LedgerHeader const& lgrInfo, + std::shared_ptr const& backend); + + /** + * @brief Get the number of subscribers of the transaction feed. + */ + std::uint64_t + transactionSubCount() const; + + /** + * @brief Get the number of accounts subscribers. + */ + std::uint64_t + accountSubCount() const; + + /** + * @brief Get the number of books subscribers. + */ + std::uint64_t + bookSubCount() const; + +private: + void + unsubInternal(SubscriberPtr subscriber); + + void + unsubInternal(ripple::AccountID const& account, SubscriberPtr subscriber); + + void + unsubInternal(ripple::Book const& book, SubscriberPtr subscriber); +}; +} // namespace feed::impl diff --git a/src/feed/impl/Util.h b/src/feed/impl/Util.h new file mode 100644 index 00000000..8b77ae60 --- /dev/null +++ b/src/feed/impl/Util.h @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +/* + 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/prometheus/Gauge.h" +#include "util/prometheus/Label.h" +#include "util/prometheus/Prometheus.h" + +#include + +#include + +namespace feed::impl { + +inline util::prometheus::GaugeInt& +getSubscriptionsGaugeInt(std::string const& counterName) +{ + return PrometheusService::gaugeInt( + "subscriptions_current_number", + util::prometheus::Labels({util::prometheus::Label{"stream", counterName}}), + fmt::format("Current subscribers number on the {} stream", counterName) + ); +} +} // namespace feed::impl diff --git a/src/main/Main.cpp b/src/main/Main.cpp index 022bdc58..4efc6f33 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -20,6 +20,7 @@ #include "data/BackendFactory.h" #include "etl/ETLHelpers.h" #include "etl/ETLService.h" +#include "feed/SubscriptionManager.h" #include "main/Build.h" #include "rpc/Counters.h" #include "rpc/RPCEngine.h" @@ -197,7 +198,9 @@ try { auto backend = data::make_Backend(config); // Manages clients subscribed to streams - auto subscriptions = feed::SubscriptionManager::make_SubscriptionManager(config, backend); + auto subscriptionsRunner = feed::SubscriptionManagerRunner(config, backend); + + auto const subscriptions = subscriptionsRunner.getManager(); // Tracks which ledgers have been validated by the network auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers(); @@ -216,14 +219,12 @@ try { auto const handlerProvider = std::make_shared( config, backend, subscriptions, balancer, etl, counters ); - auto const rpcEngine = rpc::RPCEngine::make_RPCEngine( - backend, subscriptions, balancer, dosGuard, workQueue, counters, handlerProvider - ); + auto const rpcEngine = + rpc::RPCEngine::make_RPCEngine(backend, balancer, dosGuard, workQueue, counters, handlerProvider); // Init the web server - auto handler = std::make_shared>( - config, backend, rpcEngine, etl, subscriptions - ); + auto handler = + std::make_shared>(config, backend, rpcEngine, etl); auto ctx = parseCerts(config); auto const ctxRef = ctx ? std::optional>{ctx.value()} : std::nullopt; auto const httpServer = web::make_HttpServer(config, ioc, ctxRef, dosGuard, handler); diff --git a/src/rpc/RPCEngine.h b/src/rpc/RPCEngine.h index e33f9cef..6b8f7eea 100644 --- a/src/rpc/RPCEngine.h +++ b/src/rpc/RPCEngine.h @@ -64,7 +64,6 @@ class RPCEngine { util::Logger log_{"RPC"}; std::shared_ptr backend_; - std::shared_ptr subscriptions_; std::shared_ptr balancer_; std::reference_wrapper dosGuard_; std::reference_wrapper workQueue_; @@ -77,7 +76,6 @@ class RPCEngine { public: RPCEngine( std::shared_ptr const& backend, - std::shared_ptr const& subscriptions, std::shared_ptr const& balancer, web::DOSGuard const& dosGuard, WorkQueue& workQueue, @@ -85,7 +83,6 @@ public: std::shared_ptr const& handlerProvider ) : backend_{backend} - , subscriptions_{subscriptions} , balancer_{balancer} , dosGuard_{std::cref(dosGuard)} , workQueue_{std::ref(workQueue)} @@ -98,7 +95,6 @@ public: static std::shared_ptr make_RPCEngine( std::shared_ptr const& backend, - std::shared_ptr const& subscriptions, std::shared_ptr const& balancer, web::DOSGuard const& dosGuard, WorkQueue& workQueue, @@ -106,9 +102,7 @@ public: std::shared_ptr const& handlerProvider ) { - return std::make_shared( - backend, subscriptions, balancer, dosGuard, workQueue, counters, handlerProvider - ); + return std::make_shared(backend, balancer, dosGuard, workQueue, counters, handlerProvider); } /** diff --git a/src/rpc/handlers/Subscribe.h b/src/rpc/handlers/Subscribe.h index 796e5782..5ac1a6ff 100644 --- a/src/rpc/handlers/Subscribe.h +++ b/src/rpc/handlers/Subscribe.h @@ -20,11 +20,32 @@ #pragma once #include "data/BackendInterface.h" +#include "data/Types.h" +#include "rpc/Errors.h" +#include "rpc/JS.h" #include "rpc/RPCHelpers.h" #include "rpc/common/MetaProcessors.h" #include "rpc/common/Types.h" #include "rpc/common/Validators.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + namespace feed { class SubscriptionManager; } // namespace feed @@ -125,20 +146,19 @@ public: auto output = Output{}; if (input.streams) { - auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session); + auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session, ctx.apiVersion); if (!ledger.empty()) output.ledger = ledger; } if (input.accounts) - subscribeToAccounts(*(input.accounts), ctx.session); + subscribeToAccounts(*(input.accounts), ctx.session, ctx.apiVersion); if (input.accountsProposed) subscribeToAccountsProposed(*(input.accountsProposed), ctx.session); - if (input.books) { - subscribeToBooks(*(input.books), ctx.session, ctx.yield, output); - }; + if (input.books) + subscribeToBooks(*(input.books), ctx.session, ctx.yield, ctx.apiVersion, output); return output; } @@ -148,7 +168,8 @@ private: subscribeToStreams( boost::asio::yield_context yield, std::vector const& streams, - std::shared_ptr const& session + std::shared_ptr const& session, + std::uint32_t apiVersion ) const { auto response = boost::json::object{}; @@ -157,7 +178,7 @@ private: if (stream == "ledger") { response = subscriptions_->subLedger(yield, session); } else if (stream == "transactions") { - subscriptions_->subTransactions(session); + subscriptions_->subTransactions(session, apiVersion); } else if (stream == "transactions_proposed") { subscriptions_->subProposedTransactions(session); } else if (stream == "validations") { @@ -173,12 +194,15 @@ private: } void - subscribeToAccounts(std::vector const& accounts, std::shared_ptr const& session) - const + subscribeToAccounts( + std::vector const& accounts, + std::shared_ptr const& session, + std::uint32_t apiVersion + ) const { for (auto const& account : accounts) { auto const accountID = accountFromStringStrict(account); - subscriptions_->subAccount(*accountID, session); + subscriptions_->subAccount(*accountID, session, apiVersion); } } @@ -199,6 +223,7 @@ private: std::vector const& books, std::shared_ptr const& session, boost::asio::yield_context yield, + uint32_t apiVersion, Output& output ) const { @@ -240,10 +265,10 @@ private: } } - subscriptions_->subBook(internalBook.book, session); + subscriptions_->subBook(internalBook.book, session, apiVersion); if (internalBook.both) - subscriptions_->subBook(ripple::reversed(internalBook.book), session); + subscriptions_->subBook(ripple::reversed(internalBook.book), session, apiVersion); } } diff --git a/src/web/RPCServerHandler.h b/src/web/RPCServerHandler.h index cc864fac..a63ef45f 100644 --- a/src/web/RPCServerHandler.h +++ b/src/web/RPCServerHandler.h @@ -64,8 +64,6 @@ class RPCServerHandler { std::shared_ptr const backend_; std::shared_ptr const rpcEngine_; std::shared_ptr const etl_; - // subscription manager holds the shared_ptr of this class - std::weak_ptr const subscriptions_; util::TagDecoratorFactory const tagFactory_; rpc::detail::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed @@ -86,13 +84,11 @@ public: util::Config const& config, std::shared_ptr const& backend, std::shared_ptr const& rpcEngine, - std::shared_ptr const& etl, - std::shared_ptr const& subscriptions + std::shared_ptr const& etl ) : backend_(backend) , rpcEngine_(rpcEngine) , etl_(etl) - , subscriptions_(subscriptions) , tagFactory_(config) , apiVersionParser_(config.sectionOr("api_version", {})) { @@ -140,21 +136,6 @@ public: } } - /** - * @brief The callback when there is an error. - * - * Remove the session shared ptr from subscription manager. - * - * @param ec The error code - * @param connection The connection - */ - void - operator()([[maybe_unused]] boost::beast::error_code ec, std::shared_ptr const& connection) - { - if (auto manager = subscriptions_.lock(); manager) - manager->cleanup(connection); - } - private: void handleRequest( diff --git a/src/web/impl/WsBase.h b/src/web/impl/WsBase.h index 7dc9e26a..454128b2 100644 --- a/src/web/impl/WsBase.h +++ b/src/web/impl/WsBase.h @@ -86,7 +86,6 @@ protected: if (!ec_ && ec != boost::asio::error::operation_aborted) { ec_ = ec; boost::beast::get_lowest_layer(derived().ws()).socket().close(ec); - (*handler_)(ec, derived().shared_from_this()); } } @@ -141,10 +140,6 @@ public: void maybeSendNext() { - // cleanup if needed. can't do this in destructor so it's here - if (dead()) - (*handler_)(ec_, derived().shared_from_this()); - if (ec_ || sending_ || messages_.empty()) return; diff --git a/src/web/interface/Concepts.h b/src/web/interface/Concepts.h index 6221e37c..8a101864 100644 --- a/src/web/interface/Concepts.h +++ b/src/web/interface/Concepts.h @@ -37,10 +37,6 @@ concept SomeServerHandler = { handler(req, ws) }; - // the callback when there is an error - { - handler(ec, ws) - }; }; } // namespace web diff --git a/src/web/interface/ConnectionBase.h b/src/web/interface/ConnectionBase.h index 4fd6d02b..9d7a7417 100644 --- a/src/web/interface/ConnectionBase.h +++ b/src/web/interface/ConnectionBase.h @@ -22,6 +22,7 @@ #include "util/Taggable.h" #include +#include #include @@ -42,6 +43,8 @@ public: std::string const clientIp; bool upgraded = false; bool isAdmin_ = false; + boost::signals2::signal onDisconnect; + std::uint32_t apiSubVersion = 0; /** * @brief Create a new connection base. @@ -54,7 +57,10 @@ public: { } - ~ConnectionBase() override = default; + ~ConnectionBase() override + { + onDisconnect(this); + }; /** * @brief Send the response to the client. diff --git a/unittests/SubscriptionManagerTests.cpp b/unittests/SubscriptionManagerTests.cpp deleted file mode 100644 index e71ca44d..00000000 --- a/unittests/SubscriptionManagerTests.cpp +++ /dev/null @@ -1,870 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2023, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "data/Types.h" -#include "feed/SubscriptionManager.h" -#include "util/Fixtures.h" -#include "util/MockBackend.h" -#include "util/MockWsBase.h" -#include "util/Taggable.h" -#include "util/TestObject.h" -#include "util/config/Config.h" -#include "web/interface/ConnectionBase.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using std::chrono::milliseconds; -namespace json = boost::json; -using namespace data; -using ::testing::Return; - -// common const -constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; -constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; -constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; -constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; -constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; -constexpr static auto LEDGERHASH2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; -constexpr static auto TXNID = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; - -/* - * test subscription factory method and report function - */ -TEST(SubscriptionManagerTest, InitAndReport) -{ - constexpr static auto ReportReturn = R"({ - "ledger":0, - "transactions":0, - "transactions_proposed":0, - "manifests":0, - "validations":0, - "account":0, - "accounts_proposed":0, - "books":0, - "book_changes":0 - })"; - util::Config const cfg; - auto backend = std::make_shared(cfg); - auto subManager = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend); - EXPECT_EQ(subManager->report(), json::parse(ReportReturn)); -} - -void -CheckSubscriberMessage(std::string out, std::shared_ptr session, int retry = 10) -{ - auto sessionPtr = dynamic_cast(session.get()); - ASSERT_NE(sessionPtr, nullptr); - while (retry-- != 0) { - std::this_thread::sleep_for(milliseconds(20)); - if ((!sessionPtr->message.empty()) && json::parse(sessionPtr->message) == json::parse(out)) { - return; - } - } - EXPECT_TRUE(false) << "Could not wait the subscriber message, expect:" << out << " Get:" << sessionPtr->message; -} - -// Fixture contains test target and mock backend -class SubscriptionManagerSimpleBackendTest : public MockBackendTest { -protected: - util::Config cfg; - std::shared_ptr subManagerPtr; - util::TagDecoratorFactory tagDecoratorFactory{cfg}; - std::shared_ptr session; - - void - SetUp() override - { - MockBackendTest::SetUp(); - subManagerPtr = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend); - session = std::make_shared(tagDecoratorFactory); - } - - void - TearDown() override - { - MockBackendTest::TearDown(); - subManagerPtr.reset(); - } -}; - -/* - * test report function and unsub functions - */ -TEST_F(SubscriptionManagerSimpleBackendTest, ReportCurrentSubscriber) -{ - constexpr static auto ReportReturn = R"({ - "ledger":0, - "transactions":2, - "transactions_proposed":2, - "manifests":2, - "validations":2, - "account":2, - "accounts_proposed":2, - "books":2, - "book_changes":2 - })"; - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - subManagerPtr->subBookChanges(session1); - subManagerPtr->subBookChanges(session2); - subManagerPtr->subManifest(session1); - subManagerPtr->subManifest(session2); - subManagerPtr->subProposedTransactions(session1); - subManagerPtr->subProposedTransactions(session2); - subManagerPtr->subTransactions(session1); - subManagerPtr->subTransactions(session2); - subManagerPtr->subValidation(session1); - subManagerPtr->subValidation(session2); - auto account = GetAccountIDWithString(ACCOUNT1); - subManagerPtr->subAccount(account, session1); - subManagerPtr->subAccount(account, session2); - subManagerPtr->subProposedAccount(account, session1); - subManagerPtr->subProposedAccount(account, session2); - auto issue1 = GetIssue(CURRENCY, ISSUER); - ripple::Book const book{ripple::xrpIssue(), issue1}; - subManagerPtr->subBook(book, session1); - subManagerPtr->subBook(book, session2); - std::this_thread::sleep_for(milliseconds(20)); - EXPECT_EQ(subManagerPtr->report(), json::parse(ReportReturn)); - subManagerPtr->unsubBookChanges(session1); - subManagerPtr->unsubManifest(session1); - subManagerPtr->unsubProposedTransactions(session1); - subManagerPtr->unsubTransactions(session1); - subManagerPtr->unsubValidation(session1); - subManagerPtr->unsubAccount(account, session1); - subManagerPtr->unsubProposedAccount(account, session1); - subManagerPtr->unsubBook(book, session1); - std::this_thread::sleep_for(milliseconds(20)); - auto checkResult = [](json::object reportReturn, int result) { - EXPECT_EQ(reportReturn["book_changes"], result); - EXPECT_EQ(reportReturn["validations"], result); - EXPECT_EQ(reportReturn["transactions_proposed"], result); - EXPECT_EQ(reportReturn["transactions"], result); - EXPECT_EQ(reportReturn["manifests"], result); - EXPECT_EQ(reportReturn["accounts_proposed"], result); - EXPECT_EQ(reportReturn["account"], result); - EXPECT_EQ(reportReturn["books"], result); - }; - checkResult(subManagerPtr->report(), 1); - subManagerPtr->cleanup(session2); // clean a removed session - std::this_thread::sleep_for(milliseconds(20)); - checkResult(subManagerPtr->report(), 0); -} - -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerLedgerUnSub) -{ - backend->setRange(10, 30); - boost::asio::io_context ctx; - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); - // mock fetchLedgerBySequence return this ledger - ON_CALL(*backend, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - // mock doFetchLedgerObject return fee setting ledger object - auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); - ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(feeBlob)); - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { subManagerPtr->subLedger(yield, session); }); - ctx.run(); - std::this_thread::sleep_for(milliseconds(20)); - auto report = subManagerPtr->report(); - EXPECT_EQ(report["ledger"], 1); - subManagerPtr->cleanup(session); - subManagerPtr->unsubLedger(session); - std::this_thread::sleep_for(milliseconds(20)); - report = subManagerPtr->report(); - EXPECT_EQ(report["ledger"], 0); -} - -/* - * test Manifest - * Subscription Manager forward the manifest message to subscribers - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerManifestTest) -{ - subManagerPtr->subManifest(session); - constexpr static auto dummyManifest = R"({"manifest":"test"})"; - subManagerPtr->forwardManifest(json::parse(dummyManifest).get_object()); - CheckSubscriberMessage(dummyManifest, session); -} - -/* - * test Validation - * Subscription Manager forward the validation message to subscribers - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerValidation) -{ - subManagerPtr->subValidation(session); - constexpr static auto dummyValidation = R"({"validation":"test"})"; - subManagerPtr->forwardValidation(json::parse(dummyValidation).get_object()); - CheckSubscriberMessage(dummyValidation, session); -} - -/* - * test ProposedTransaction - * We don't need the valid transaction in this test, subscription manager just - * forward the message to subscriber - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerProposedTransaction) -{ - subManagerPtr->subProposedTransactions(session); - // transaction contains account and its public key - // make sure it is not parsed to two identical accounts - constexpr static auto dummyTransaction = R"({ - "transaction": - { - "Account":"rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb", - "Amount":"40000000", - "Destination":"rDgGprMjMWkJRnJ8M5RXq3SXYD8zuQncPc", - "Fee":"20", - "Flags":2147483648, - "Sequence":13767283, - "SigningPubKey":"036F3CFFE1EA77C1EEC5DCCA38C83E62E3AC068F8A16369620AF1D609BA5A620B2", - "TransactionType":"Payment", - "TxnSignature":"30450221009BD0D563B24E50B26A42F30455AD21C3D5CD4D80174C41F7B54969FFC08DE94C02201FC35320B56D56D1E34D1D281D48AC68CBEDDD6EE9DFA639CCB08BB251453A87", - "hash":"F44393295DB860C6860769C16F5B23887762F09F87A8D1174E0FCFF9E7247F07" - } - })"; - subManagerPtr->forwardProposedTransaction(json::parse(dummyTransaction).get_object()); - CheckSubscriberMessage(dummyTransaction, session); -} - -/* - * test ProposedTransaction for one account - * we need to construct a valid account in the transaction - * this test subscribe the proposed transaction for two accounts - * but only forward a transaction with one of them - * check the correct session is called - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerAccountProposedTransaction) -{ - auto account = GetAccountIDWithString(ACCOUNT1); - subManagerPtr->subProposedAccount(account, session); - - std::shared_ptr const sessionIdle = std::make_shared(tagDecoratorFactory); - auto accountIdle = GetAccountIDWithString(ACCOUNT2); - subManagerPtr->subProposedAccount(accountIdle, sessionIdle); - - constexpr static auto dummyTransaction = R"({ - "transaction": - { - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" - } - })"; - subManagerPtr->forwardProposedTransaction(json::parse(dummyTransaction).get_object()); - CheckSubscriberMessage(dummyTransaction, session); - auto rawIdle = dynamic_cast(sessionIdle.get()); - ASSERT_NE(rawIdle, nullptr); - EXPECT_EQ("", rawIdle->message); -} - -/* - * test ledger stream - * check 1 subscribe response, 2 publish message - * mock backend to return fee ledger object - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerLedger) -{ - backend->setRange(10, 30); - boost::asio::io_context ctx; - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); - // mock fetchLedgerBySequence return this ledger - ON_CALL(*backend, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - // mock doFetchLedgerObject return fee setting ledger object - auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); - ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(feeBlob)); - EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); - // check the function response - // Information about the ledgers on hand and current fee schedule. This - // includes the same fields as a ledger stream message, except that it omits - // the type and txn_count fields - constexpr static auto LedgerResponse = R"({ - "validated_ledgers":"10-30", - "ledger_index":30, - "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", - "ledger_time":0, - "fee_base":1, - "reserve_base":3, - "reserve_inc":2 - })"; - boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { - auto res = subManagerPtr->subLedger(yield, session); - // check the response - EXPECT_EQ(res, json::parse(LedgerResponse)); - }); - ctx.run(); - // test publish - auto ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31); - auto fee2 = ripple::Fees(); - fee2.reserve = 10; - subManagerPtr->pubLedger(ledgerinfo2, fee2, "10-31", 8); - constexpr static auto LedgerPub = R"({ - "type":"ledgerClosed", - "ledger_index":31, - "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", - "ledger_time":0, - "fee_base":0, - "reserve_base":10, - "reserve_inc":0, - "validated_ledgers":"10-31", - "txn_count":8 - })"; - CheckSubscriberMessage(LedgerPub, session); -} - -/* - * test book change - * create a book change meta data for - * XRP vs A token - * the transaction is just placeholder - * Book change computing only needs meta data - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerBookChange) -{ - subManagerPtr->subBookChanges(session); - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32); - auto transactions = std::vector{}; - auto trans1 = TransactionAndMetadata(); - ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - ripple::STObject const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1); - trans1.metadata = metaObj.getSerializer().peekData(); - transactions.push_back(trans1); - subManagerPtr->pubBookChanges(ledgerinfo, transactions); - constexpr static auto BookChangePublish = R"({ - "type":"bookChanges", - "ledger_index":32, - "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", - "ledger_time":0, - "changes":[ - { - "currency_a":"XRP_drops", - "currency_b":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD/0158415500000000C1F76FF6ECB0BAC600000000", - "volume_a":"2", - "volume_b":"2", - "high":"-1", - "low":"-1", - "open":"-1", - "close":"-1" - } - ] - })"; - CheckSubscriberMessage(BookChangePublish, session, 20); -} - -/* - * test transaction stream - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerTransaction) -{ - subManagerPtr->subTransactions(session); - - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - - auto trans1 = TransactionAndMetadata(); - ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - // create an empty meta object - ripple::STArray const metaArray{0}; - ripple::STObject metaObj(ripple::sfTransactionMetaData); - metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); - metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); - metaObj.setFieldU32(ripple::sfTransactionIndex, 22); - trans1.metadata = metaObj.getSerializer().peekData(); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - constexpr static auto TransactionPublish = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount":"1", - "DeliverMax":"1", - "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TransactionType":"Payment", - "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "date":0 - }, - "meta":{ - "AffectedNodes":[], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS", - "delivered_amount":"unavailable" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "close_time_iso": "2000-01-01T00:00:00Z", - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "engine_result":"tesSUCCESS", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - CheckSubscriberMessage(TransactionPublish, session); -} - -/* - * test transaction for offer creation - * check owner_funds - * mock backend return a trustline - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerTransactionOfferCreation) -{ - subManagerPtr->subTransactions(session); - - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - auto trans1 = TransactionAndMetadata(); - ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - ripple::STArray const metaArray{0}; - ripple::STObject metaObj(ripple::sfTransactionMetaData); - metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); - metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); - metaObj.setFieldU32(ripple::sfTransactionIndex, 22); - trans1.metadata = metaObj.getSerializer().peekData(); - - ripple::STObject line(ripple::sfIndexes); - line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); - line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); - line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); - line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); - line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); - line.setFieldU32(ripple::sfFlags, 0); - auto issue2 = GetIssue(CURRENCY, ISSUER); - line.setFieldAmount(ripple::sfBalance, ripple::STAmount(issue2, 100)); - - EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); - ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(line.getSerializer().peekData())); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - constexpr static auto TransactionForOwnerFund = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TakerGets":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"1" - }, - "TakerPays":"3", - "TransactionType":"OfferCreate", - "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", - "date":0, - "owner_funds":"100" - }, - "meta":{ - "AffectedNodes":[], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "close_time_iso": "2000-01-01T00:00:00Z", - "engine_result":"tesSUCCESS", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - CheckSubscriberMessage(TransactionForOwnerFund, session); -} - -constexpr static auto TransactionForOwnerFundFrozen = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TakerGets":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"1" - }, - "TakerPays":"3", - "TransactionType":"OfferCreate", - "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", - "date":0, - "owner_funds":"0" - }, - "meta":{ - "AffectedNodes":[], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "close_time_iso": "2000-01-01T00:00:00Z", - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "engine_result":"tesSUCCESS", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." -})"; - -/* - * test transaction for offer creation - * check owner_funds when line is frozen - * mock backend return a trustline - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerTransactionOfferCreationFrozenLine) -{ - subManagerPtr->subTransactions(session); - - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - auto trans1 = TransactionAndMetadata(); - ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - ripple::STArray const metaArray{0}; - ripple::STObject metaObj(ripple::sfTransactionMetaData); - metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); - metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); - metaObj.setFieldU32(ripple::sfTransactionIndex, 22); - trans1.metadata = metaObj.getSerializer().peekData(); - - ripple::STObject line(ripple::sfIndexes); - line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); - line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); - line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); - line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); - line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); - line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); - line.setFieldAmount(ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); - - EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); - ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(line.getSerializer().peekData())); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - CheckSubscriberMessage(TransactionForOwnerFundFrozen, session); -} - -/* - * test transaction for offer creation - * check owner_funds when issue global frozen - * mock backend return a frozen account setting - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerTransactionOfferCreationGlobalFrozen) -{ - subManagerPtr->subTransactions(session); - - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - auto trans1 = TransactionAndMetadata(); - ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - ripple::STArray const metaArray{0}; - ripple::STObject metaObj(ripple::sfTransactionMetaData); - metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); - metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); - metaObj.setFieldU32(ripple::sfTransactionIndex, 22); - trans1.metadata = metaObj.getSerializer().peekData(); - - ripple::STObject line(ripple::sfIndexes); - line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); - line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); - line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); - line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); - line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); - line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); - auto issueAccount = GetAccountIDWithString(ISSUER); - line.setFieldAmount(ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); - - EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); - auto kk = ripple::keylet::account(issueAccount).key; - ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_)) - .WillByDefault(Return(line.getSerializer().peekData())); - ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, ripple::lsfGlobalFreeze, 1, 10, 2, TXNID, 3); - ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_)) - .WillByDefault(Return(accountRoot.getSerializer().peekData())); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - CheckSubscriberMessage(TransactionForOwnerFundFrozen, session); -} - -/* - * test subscribe account - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerAccount) -{ - auto account = GetAccountIDWithString(ACCOUNT1); - subManagerPtr->subAccount(account, session); - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - - ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); - auto trans1 = TransactionAndMetadata(); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - ripple::STArray metaArray{1}; - ripple::STObject node(ripple::sfModifiedNode); - // emplace account into meta, trigger publish - ripple::STObject finalFields(ripple::sfFinalFields); - finalFields.setAccountID(ripple::sfAccount, account); - node.emplace_back(finalFields); - node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT); - metaArray.push_back(node); - ripple::STObject metaObj(ripple::sfTransactionMetaData); - metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); - metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); - metaObj.setFieldU32(ripple::sfTransactionIndex, 22); - trans1.metadata = metaObj.getSerializer().peekData(); - - subManagerPtr->pubTransaction(trans1, ledgerinfo); - constexpr static auto AccountPublish = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount":"1", - "DeliverMax":"1", - "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TransactionType":"Payment", - "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "date":0 - }, - "meta":{ - "AffectedNodes":[ - { - "ModifiedNode":{ - "FinalFields":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" - }, - "LedgerEntryType":"AccountRoot" - } - } - ], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS", - "delivered_amount":"unavailable" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "close_time_iso": "2000-01-01T00:00:00Z", - "engine_result":"tesSUCCESS", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - CheckSubscriberMessage(AccountPublish, session); -} - -/* - * test subscribe order book - * Create/Delete/Update offer node will trigger publish - */ -TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerOrderBook) -{ - auto issue1 = GetIssue(CURRENCY, ISSUER); - ripple::Book const book{ripple::xrpIssue(), issue1}; - subManagerPtr->subBook(book, session); - auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); - - auto trans1 = TransactionAndMetadata(); - auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); - trans1.transaction = obj.getSerializer().peekData(); - trans1.ledgerSequence = 32; - - auto metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3); - trans1.metadata = metaObj.getSerializer().peekData(); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - - constexpr static auto OrderbookPublish = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount":"1", - "DeliverMax":"1", - "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TransactionType":"Payment", - "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "date":0 - }, - "meta":{ - "AffectedNodes":[ - { - "ModifiedNode":{ - "FinalFields":{ - "TakerGets":"3", - "TakerPays":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"1" - } - }, - "LedgerEntryType":"Offer", - "PreviousFields":{ - "TakerGets":"1", - "TakerPays":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"3" - } - } - } - } - ], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS", - "delivered_amount":"unavailable" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "engine_result":"tesSUCCESS", - "close_time_iso": "2000-01-01T00:00:00Z", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - CheckSubscriberMessage(OrderbookPublish, session); - - // trigger by offer cancel meta data - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - subManagerPtr->subBook(book, session1); - metaObj = CreateMetaDataForCancelOffer(CURRENCY, ISSUER, 22, 3, 1); - trans1.metadata = metaObj.getSerializer().peekData(); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - constexpr static auto OrderbookCancelPublish = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount":"1", - "DeliverMax":"1", - "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TransactionType":"Payment", - "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "date":0 - }, - "meta":{ - "AffectedNodes":[ - { - "DeletedNode":{ - "FinalFields":{ - "TakerGets":"3", - "TakerPays":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"1" - } - }, - "LedgerEntryType":"Offer" - } - } - ], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS", - "delivered_amount":"unavailable" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "engine_result":"tesSUCCESS", - "close_time_iso": "2000-01-01T00:00:00Z", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - CheckSubscriberMessage(OrderbookCancelPublish, session1); - // trigger by offer create meta data - constexpr static auto OrderbookCreatePublish = R"({ - "transaction":{ - "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount":"1", - "DeliverMax":"1", - "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee":"1", - "Sequence":32, - "SigningPubKey":"74657374", - "TransactionType":"Payment", - "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "date":0 - }, - "meta":{ - "AffectedNodes":[ - { - "CreatedNode":{ - "NewFields":{ - "TakerGets":"3", - "TakerPays":{ - "currency":"0158415500000000C1F76FF6ECB0BAC600000000", - "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", - "value":"1" - } - }, - "LedgerEntryType":"Offer" - } - } - ], - "TransactionIndex":22, - "TransactionResult":"tesSUCCESS", - "delivered_amount":"unavailable" - }, - "type":"transaction", - "validated":true, - "status":"closed", - "ledger_index":33, - "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", - "engine_result_code":0, - "engine_result":"tesSUCCESS", - "close_time_iso": "2000-01-01T00:00:00Z", - "engine_result_message":"The transaction was applied. Only final in a validated ledger." - })"; - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - subManagerPtr->subBook(book, session2); - metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1); - trans1.metadata = metaObj.getSerializer().peekData(); - subManagerPtr->pubTransaction(trans1, ledgerinfo); - CheckSubscriberMessage(OrderbookCreatePublish, session2); -} diff --git a/unittests/SubscriptionTests.cpp b/unittests/SubscriptionTests.cpp deleted file mode 100644 index 52fc5d7a..00000000 --- a/unittests/SubscriptionTests.cpp +++ /dev/null @@ -1,314 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2023, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "feed/SubscriptionManager.h" -#include "util/Fixtures.h" -#include "util/MockPrometheus.h" -#include "util/MockWsBase.h" -#include "util/Taggable.h" -#include "util/config/Config.h" -#include "util/prometheus/Gauge.h" -#include "web/interface/ConnectionBase.h" - -#include -#include - -#include -#include - -using namespace feed; -using namespace util::prometheus; - -// io_context -struct SubscriptionTestBase { - util::Config cfg; - util::TagDecoratorFactory tagDecoratorFactory{cfg}; -}; - -struct SubscriptionTest : WithPrometheus, SyncAsioContextTest, SubscriptionTestBase { - Subscription sub{ctx, "test"}; -}; - -// subscribe/unsubscribe the same session would not change the count -TEST_F(SubscriptionTest, SubscriptionCount) -{ - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - sub.subscribe(session1); - sub.subscribe(session2); - ctx.run(); - EXPECT_EQ(sub.count(), 2); - sub.subscribe(session1); - ctx.restart(); - ctx.run(); - EXPECT_EQ(sub.count(), 2); - EXPECT_TRUE(sub.hasSession(session1)); - EXPECT_TRUE(sub.hasSession(session2)); - EXPECT_FALSE(sub.empty()); - sub.unsubscribe(session1); - ctx.restart(); - ctx.run(); - EXPECT_EQ(sub.count(), 1); - sub.unsubscribe(session1); - ctx.restart(); - ctx.run(); - EXPECT_EQ(sub.count(), 1); - sub.unsubscribe(session2); - ctx.restart(); - ctx.run(); - EXPECT_EQ(sub.count(), 0); - EXPECT_TRUE(sub.empty()); - EXPECT_FALSE(sub.hasSession(session1)); - EXPECT_FALSE(sub.hasSession(session2)); -} - -// send interface will be called when publish called -TEST_F(SubscriptionTest, SubscriptionPublish) -{ - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - sub.subscribe(session1); - sub.subscribe(session2); - ctx.run(); - EXPECT_EQ(sub.count(), 2); - sub.publish(std::make_shared("message")); - ctx.restart(); - ctx.run(); - MockSession* p1 = dynamic_cast(session1.get()); - ASSERT_NE(p1, nullptr); - EXPECT_EQ(p1->message, "message"); - MockSession* p2 = dynamic_cast(session2.get()); - ASSERT_NE(p2, nullptr); - EXPECT_EQ(p2->message, "message"); - sub.unsubscribe(session1); - ctx.restart(); - ctx.run(); - sub.publish(std::make_shared("message2")); - ctx.restart(); - ctx.run(); - EXPECT_EQ(p1->message, "message"); - EXPECT_EQ(p2->message, "messagemessage2"); -} - -// when error happen during send(), the subsciber will be removed after -TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber) -{ - std::shared_ptr const session1(new MockDeadSession(tagDecoratorFactory)); - sub.subscribe(session1); - ctx.run(); - EXPECT_EQ(sub.count(), 1); - // trigger dead - sub.publish(std::make_shared("message")); - ctx.restart(); - ctx.run(); - EXPECT_EQ(session1->dead(), true); - sub.publish(std::make_shared("message")); - ctx.restart(); - ctx.run(); - EXPECT_EQ(sub.count(), 0); -} - -struct SubscriptionMockPrometheusTest : WithMockPrometheus, SubscriptionTestBase, SyncAsioContextTest { - Subscription sub{ctx, "test"}; - std::shared_ptr const session = std::make_shared(tagDecoratorFactory); -}; - -TEST_F(SubscriptionMockPrometheusTest, subscribe) -{ - auto& counter = makeMock("subscriptions_current_number", "{stream=\"test\"}"); - EXPECT_CALL(counter, add(1)); - sub.subscribe(session); - ctx.run(); -} - -TEST_F(SubscriptionMockPrometheusTest, unsubscribe) -{ - auto& counter = makeMock("subscriptions_current_number", "{stream=\"test\"}"); - EXPECT_CALL(counter, add(1)); - sub.subscribe(session); - ctx.run(); - EXPECT_CALL(counter, add(-1)); - sub.unsubscribe(session); - ctx.restart(); - ctx.run(); -} - -TEST_F(SubscriptionMockPrometheusTest, publish) -{ - auto deadSession = std::make_shared(tagDecoratorFactory); - auto& counter = makeMock("subscriptions_current_number", "{stream=\"test\"}"); - EXPECT_CALL(counter, add(1)); - sub.subscribe(deadSession); - ctx.run(); - EXPECT_CALL(counter, add(-1)); - sub.publish(std::make_shared("message")); - sub.publish(std::make_shared("message")); // Dead session is detected only after failed send - ctx.restart(); - ctx.run(); -} - -TEST_F(SubscriptionMockPrometheusTest, count) -{ - auto& counter = makeMock("subscriptions_current_number", "{stream=\"test\"}"); - EXPECT_CALL(counter, value()); - sub.count(); -} - -struct SubscriptionMapTest : SubscriptionTest { - SubscriptionMap subMap{ctx, "test"}; -}; - -TEST_F(SubscriptionMapTest, SubscriptionMapCount) -{ - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session3 = std::make_shared(tagDecoratorFactory); - subMap.subscribe(session1, "topic1"); - subMap.subscribe(session2, "topic1"); - subMap.subscribe(session3, "topic2"); - ctx.run(); - EXPECT_EQ(subMap.count(), 3); - subMap.subscribe(session1, "topic1"); - subMap.subscribe(session2, "topic1"); - ctx.restart(); - ctx.run(); - EXPECT_EQ(subMap.count(), 3); - EXPECT_TRUE(subMap.hasSession(session1, "topic1")); - EXPECT_TRUE(subMap.hasSession(session2, "topic1")); - EXPECT_TRUE(subMap.hasSession(session3, "topic2")); - subMap.unsubscribe(session1, "topic1"); - ctx.restart(); - ctx.run(); - subMap.unsubscribe(session1, "topic1"); - subMap.unsubscribe(session2, "topic1"); - subMap.unsubscribe(session3, "topic2"); - ctx.restart(); - ctx.run(); - EXPECT_FALSE(subMap.hasSession(session1, "topic1")); - EXPECT_FALSE(subMap.hasSession(session2, "topic1")); - EXPECT_FALSE(subMap.hasSession(session3, "topic2")); - EXPECT_EQ(subMap.count(), 0); - subMap.unsubscribe(session3, "topic2"); - subMap.unsubscribe(session3, "no exist"); - ctx.restart(); - ctx.run(); - EXPECT_EQ(subMap.count(), 0); -} - -TEST_F(SubscriptionMapTest, SubscriptionMapPublish) -{ - std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - std::string const topic1 = "topic1"; - std::string const topic2 = "topic2"; - std::string const topic1Message = "topic1Message"; - std::string const topic2Message = "topic2Message"; - subMap.subscribe(session1, topic1); - subMap.subscribe(session2, topic2); - ctx.run(); - EXPECT_EQ(subMap.count(), 2); - auto message1 = std::make_shared(topic1Message.data()); - subMap.publish(message1, topic1); // lvalue - subMap.publish(std::make_shared(topic2Message.data()), topic2); // rvalue - ctx.restart(); - ctx.run(); - MockSession* p1 = dynamic_cast(session1.get()); - ASSERT_NE(p1, nullptr); - EXPECT_EQ(p1->message, topic1Message); - MockSession* p2 = dynamic_cast(session2.get()); - ASSERT_NE(p2, nullptr); - EXPECT_EQ(p2->message, topic2Message); -} - -TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber) -{ - std::shared_ptr const session1(new MockDeadSession(tagDecoratorFactory)); - std::shared_ptr const session2 = std::make_shared(tagDecoratorFactory); - std::string const topic1 = "topic1"; - std::string const topic2 = "topic2"; - std::string const topic1Message = "topic1Message"; - std::string const topic2Message = "topic2Message"; - subMap.subscribe(session1, topic1); - subMap.subscribe(session2, topic2); - ctx.run(); - EXPECT_EQ(subMap.count(), 2); - auto message1 = std::make_shared(topic1Message); - subMap.publish(message1, topic1); // lvalue - subMap.publish(std::make_shared(topic2Message), topic2); // rvalue - ctx.restart(); - ctx.run(); - MockDeadSession* p1 = dynamic_cast(session1.get()); - ASSERT_NE(p1, nullptr); - EXPECT_EQ(p1->dead(), true); - MockSession* p2 = dynamic_cast(session2.get()); - ASSERT_NE(p2, nullptr); - EXPECT_EQ(p2->message, topic2Message); - subMap.publish(message1, topic1); - ctx.restart(); - ctx.run(); - EXPECT_EQ(subMap.count(), 1); -} - -struct SubscriptionMapMockPrometheusTest : SubscriptionMockPrometheusTest { - SubscriptionMap subMap{ctx, "test"}; - std::shared_ptr const session = std::make_shared(tagDecoratorFactory); -}; - -TEST_F(SubscriptionMapMockPrometheusTest, subscribe) -{ - auto& counter = makeMock("subscriptions_current_number", "{collection=\"test\"}"); - EXPECT_CALL(counter, add(1)); - subMap.subscribe(session, "topic"); - ctx.run(); -} - -TEST_F(SubscriptionMapMockPrometheusTest, unsubscribe) -{ - auto& counter = makeMock("subscriptions_current_number", "{collection=\"test\"}"); - EXPECT_CALL(counter, add(1)); - subMap.subscribe(session, "topic"); - ctx.run(); - EXPECT_CALL(counter, add(-1)); - subMap.unsubscribe(session, "topic"); - ctx.restart(); - ctx.run(); -} - -TEST_F(SubscriptionMapMockPrometheusTest, publish) -{ - auto deadSession = std::make_shared(tagDecoratorFactory); - auto& counter = makeMock("subscriptions_current_number", "{collection=\"test\"}"); - EXPECT_CALL(counter, add(1)); - subMap.subscribe(deadSession, "topic"); - ctx.run(); - EXPECT_CALL(counter, add(-1)); - subMap.publish(std::make_shared("message"), "topic"); - subMap.publish( - std::make_shared("message"), "topic" - ); // Dead session is detected only after failed send - ctx.restart(); - ctx.run(); -} - -TEST_F(SubscriptionMapMockPrometheusTest, count) -{ - auto& counter = makeMock("subscriptions_current_number", "{collection=\"test\"}"); - EXPECT_CALL(counter, value()); - subMap.count(); -} diff --git a/unittests/feed/BookChangesFeedTests.cpp b/unittests/feed/BookChangesFeedTests.cpp new file mode 100644 index 00000000..c9db6a88 --- /dev/null +++ b/unittests/feed/BookChangesFeedTests.cpp @@ -0,0 +1,92 @@ +//------------------------------------------------------------------------------ +/* + 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 "data/Types.h" +#include "feed/FeedBaseTest.h" +#include "feed/impl/BookChangesFeed.h" +#include "feed/impl/ForwardFeed.h" +#include "util/TestObject.h" + +#include +#include +#include +#include + +#include +#include + +using namespace feed::impl; +namespace json = boost::json; + +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; + +using FeedBookChangeTest = FeedBaseTest; + +TEST_F(FeedBookChangeTest, Pub) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32); + auto transactions = std::vector{}; + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STObject const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + transactions.push_back(trans1); + + testFeedPtr->pub(ledgerinfo, transactions); + constexpr static auto bookChangePublish = + R"({ + "type":"bookChanges", + "ledger_index":32, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "changes": + [ + { + "currency_a":"XRP_drops", + "currency_b":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD/0158415500000000C1F76FF6ECB0BAC600000000", + "volume_a":"2", + "volume_b":"2", + "high":"-1", + "low":"-1", + "open":"-1", + "close":"-1" + } + ] + })"; + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(bookChangePublish)); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); + cleanReceivedFeed(); + testFeedPtr->pub(ledgerinfo, transactions); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} diff --git a/unittests/feed/FeedBaseTest.h b/unittests/feed/FeedBaseTest.h new file mode 100644 index 00000000..fb427ca6 --- /dev/null +++ b/unittests/feed/FeedBaseTest.h @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + 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/Fixtures.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/config/Config.h" +#include "web/interface/ConnectionBase.h" + +#include + +#include +#include + +// Base class for feed tests, providing easy way to access the received feed +template +class FeedBaseTest : public SyncAsioContextTest, public MockBackendTest { +protected: + util::TagDecoratorFactory tagDecoratorFactory{util::Config{}}; + std::shared_ptr sessionPtr; + std::shared_ptr testFeedPtr; + + void + SetUp() override + { + SyncAsioContextTest::SetUp(); + MockBackendTest::SetUp(); + testFeedPtr = std::make_shared(ctx); + sessionPtr = std::make_shared(tagDecoratorFactory); + } + + void + TearDown() override + { + sessionPtr.reset(); + testFeedPtr.reset(); + MockBackendTest::TearDown(); + SyncAsioContextTest::TearDown(); + } + + std::string const& + receivedFeedMessage() const + { + auto const mockSession = dynamic_cast(sessionPtr.get()); + [&] { ASSERT_NE(mockSession, nullptr); }(); + return mockSession->message; + } + + void + cleanReceivedFeed() + { + auto mockSession = dynamic_cast(sessionPtr.get()); + [&] { ASSERT_NE(mockSession, nullptr); }(); + mockSession->message.clear(); + } +}; diff --git a/unittests/feed/ForwardFeedTests.cpp b/unittests/feed/ForwardFeedTests.cpp new file mode 100644 index 00000000..954beef7 --- /dev/null +++ b/unittests/feed/ForwardFeedTests.cpp @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/FeedBaseTest.h" +#include "feed/impl/ForwardFeed.h" + +#include +#include +#include + +#include + +using namespace feed::impl; +namespace json = boost::json; +using namespace util::prometheus; + +constexpr static auto FEED = R"({"test":"test"})"; + +class NamedForwardFeedTest : public ForwardFeed { +public: + NamedForwardFeedTest(boost::asio::io_context& ioContext) : ForwardFeed(ioContext, "test") + { + } +}; + +using FeedForwardTest = FeedBaseTest; + +TEST_F(FeedForwardTest, Pub) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + auto const json = json::parse(FEED).as_object(); + testFeedPtr->pub(json); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage(), FEED); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); + cleanReceivedFeed(); + testFeedPtr->pub(json); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedForwardTest, AutoDisconnect) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + auto const json = json::parse(FEED).as_object(); + testFeedPtr->pub(json); + ctx.run(); + EXPECT_EQ(receivedFeedMessage(), FEED); + sessionPtr.reset(); + EXPECT_EQ(testFeedPtr->count(), 0); + testFeedPtr->pub(json); +} diff --git a/unittests/feed/LedgerFeedTests.cpp b/unittests/feed/LedgerFeedTests.cpp new file mode 100644 index 00000000..38dc015f --- /dev/null +++ b/unittests/feed/LedgerFeedTests.cpp @@ -0,0 +1,141 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/FeedBaseTest.h" +#include "feed/impl/LedgerFeed.h" +#include "util/Fixtures.h" +#include "util/TestObject.h" + +#include +#include +#include +#include +#include +#include + +#include + +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; + +using namespace feed::impl; +namespace json = boost::json; + +using FeedLedgerTest = FeedBaseTest; + +TEST_F(FeedLedgerTest, SubPub) +{ + backend->setRange(10, 30); + auto const ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerInfo)); + + auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); + EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob)); + // check the function response + // Information about the ledgers on hand and current fee schedule. This + // includes the same fields as a ledger stream message, except that it omits + // the type and txn_count fields + constexpr static auto LedgerResponse = + R"({ + "validated_ledgers":"10-30", + "ledger_index":30, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_base":1, + "reserve_base":3, + "reserve_inc":2 + })"; + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto res = testFeedPtr->sub(yield, backend, sessionPtr); + // check the response + EXPECT_EQ(res, json::parse(LedgerResponse)); + }); + ctx.run(); + EXPECT_EQ(testFeedPtr->count(), 1); + + // test publish + auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31); + auto fee2 = ripple::Fees(); + fee2.reserve = 10; + testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8); + constexpr static auto ledgerPub = + R"({ + "type":"ledgerClosed", + "ledger_index":31, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_base":0, + "reserve_base":10, + "reserve_inc":0, + "validated_ledgers":"10-31", + "txn_count":8 + })"; + ctx.restart(); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(ledgerPub)); + + // test unsub + cleanReceivedFeed(); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); + + testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedLedgerTest, AutoDisconnect) +{ + backend->setRange(10, 30); + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerinfo)); + + auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); + EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob)); + constexpr static auto LedgerResponse = + R"({ + "validated_ledgers":"10-30", + "ledger_index":30, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_base":1, + "reserve_base":3, + "reserve_inc":2 + })"; + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto res = testFeedPtr->sub(yield, backend, sessionPtr); + // check the response + EXPECT_EQ(res, json::parse(LedgerResponse)); + }); + ctx.run(); + EXPECT_EQ(testFeedPtr->count(), 1); + + // destroy the session + sessionPtr.reset(); + EXPECT_EQ(testFeedPtr->count(), 0); + + auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31); + auto fee2 = ripple::Fees(); + fee2.reserve = 10; + // no error + testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8); + ctx.restart(); + ctx.run(); +} diff --git a/unittests/feed/ProposedTransactionFeedTests.cpp b/unittests/feed/ProposedTransactionFeedTests.cpp new file mode 100644 index 00000000..b92b4b62 --- /dev/null +++ b/unittests/feed/ProposedTransactionFeedTests.cpp @@ -0,0 +1,325 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/FeedBaseTest.h" +#include "feed/impl/ProposedTransactionFeed.h" +#include "util/Fixtures.h" +#include "util/MockPrometheus.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/TestObject.h" +#include "util/config/Config.h" +#include "util/prometheus/Gauge.h" +#include "web/interface/ConnectionBase.h" + +#include +#include +#include +#include +#include + +#include + +constexpr static auto ACCOUNT1 = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto DUMMY_TRANSACTION = + R"({ + "transaction": + { + "Account":"rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb", + "Amount":"40000000", + "Destination":"rDgGprMjMWkJRnJ8M5RXq3SXYD8zuQncPc", + "Fee":"20", + "Flags":2147483648, + "Sequence":13767283, + "SigningPubKey":"036F3CFFE1EA77C1EEC5DCCA38C83E62E3AC068F8A16369620AF1D609BA5A620B2", + "TransactionType":"Payment", + "TxnSignature":"30450221009BD0D563B24E50B26A42F30455AD21C3D5CD4D80174C41F7B54969FFC08DE94C02201FC35320B56D56D1E34D1D281D48AC68CBEDDD6EE9DFA639CCB08BB251453A87", + "hash":"F44393295DB860C6860769C16F5B23887762F09F87A8D1174E0FCFF9E7247F07" + } + })"; + +using namespace feed::impl; +namespace json = boost::json; +using namespace util::prometheus; + +using FeedProposedTransactionTest = FeedBaseTest; + +TEST_F(FeedProposedTransactionTest, ProposedTransaction) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(DUMMY_TRANSACTION)); + + cleanReceivedFeed(); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 0); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedProposedTransactionTest, AccountProposedTransaction) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + std::shared_ptr const sessionIdle = std::make_shared(tagDecoratorFactory); + auto const accountIdle = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(accountIdle, sessionIdle); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(DUMMY_TRANSACTION)); + + auto const rawIdle = dynamic_cast(sessionIdle.get()); + ASSERT_NE(rawIdle, nullptr); + EXPECT_TRUE(rawIdle->message.empty()); + + // unsub + cleanReceivedFeed(); + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedProposedTransactionTest, SubStreamAndAccount) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr); + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size() * 2); + + cleanReceivedFeed(); + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size() * 2); + + // unsub + cleanReceivedFeed(); + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size()); + + // unsub transaction + cleanReceivedFeed(); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 0); + + testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + auto const account2 = GetAccountIDWithString(ACCOUNT2); + + testFeedPtr->sub(account, sessionPtr); + testFeedPtr->sub(account2, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + constexpr static auto dummyTransaction = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + } + })"; + + testFeedPtr->pub(json::parse(dummyTransaction).get_object()); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyTransaction)); + + // unsub account1 + cleanReceivedFeed(); + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + testFeedPtr->pub(json::parse(dummyTransaction).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyTransaction)); + + // unsub account2 + cleanReceivedFeed(); + testFeedPtr->unsub(account2, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + testFeedPtr->pub(json::parse(dummyTransaction).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedProposedTransactionTest, Count) +{ + testFeedPtr->sub(sessionPtr); + // repeat + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + auto const account1 = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account1, sessionPtr); + // repeat + testFeedPtr->sub(account1, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + auto const sessionPtr2 = std::make_shared(tagDecoratorFactory); + testFeedPtr->sub(sessionPtr2); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 2); + + auto const account2 = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(account2, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + testFeedPtr->sub(account1, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 3); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + // unsub unsubscribed account + testFeedPtr->unsub(account2, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 3); + + testFeedPtr->unsub(account1, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + testFeedPtr->unsub(account1, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + testFeedPtr->unsub(account2, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); +} + +TEST_F(FeedProposedTransactionTest, AutoDisconnect) +{ + testFeedPtr->sub(sessionPtr); + // repeat + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + auto const account1 = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account1, sessionPtr); + // repeat + testFeedPtr->sub(account1, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + auto sessionPtr2 = std::make_shared(tagDecoratorFactory); + testFeedPtr->sub(sessionPtr2); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 2); + + auto const account2 = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(account2, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + testFeedPtr->sub(account1, sessionPtr2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 3); + + sessionPtr2.reset(); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 1); + + sessionPtr.reset(); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + EXPECT_EQ(testFeedPtr->transactionSubcount(), 0); +} + +struct ProposedTransactionFeedMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest { +protected: + util::TagDecoratorFactory tagDecoratorFactory{util::Config{}}; + std::shared_ptr sessionPtr; + std::shared_ptr testFeedPtr; + + void + SetUp() override + { + SyncAsioContextTest::SetUp(); + testFeedPtr = std::make_shared(ctx); + sessionPtr = std::make_shared(tagDecoratorFactory); + } + void + TearDown() override + { + sessionPtr.reset(); + testFeedPtr.reset(); + SyncAsioContextTest::TearDown(); + } +}; + +TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub) +{ + auto& counterTx = makeMock("subscriptions_current_number", "{stream=\"tx_proposed\"}"); + auto& counterAccount = makeMock("subscriptions_current_number", "{stream=\"account_proposed\"}"); + + EXPECT_CALL(counterTx, add(1)); + EXPECT_CALL(counterTx, add(-1)); + EXPECT_CALL(counterAccount, add(1)); + EXPECT_CALL(counterAccount, add(-1)); + + testFeedPtr->sub(sessionPtr); + testFeedPtr->unsub(sessionPtr); + + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr); + testFeedPtr->unsub(account, sessionPtr); +} + +TEST_F(ProposedTransactionFeedMockPrometheusTest, AutoDisconnect) +{ + auto& counterTx = makeMock("subscriptions_current_number", "{stream=\"tx_proposed\"}"); + auto& counterAccount = makeMock("subscriptions_current_number", "{stream=\"account_proposed\"}"); + + EXPECT_CALL(counterTx, add(1)); + EXPECT_CALL(counterTx, add(-1)); + EXPECT_CALL(counterAccount, add(1)); + EXPECT_CALL(counterAccount, add(-1)); + + testFeedPtr->sub(sessionPtr); + + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr); + + sessionPtr.reset(); +} diff --git a/unittests/feed/SingleFeedBaseTests.cpp b/unittests/feed/SingleFeedBaseTests.cpp new file mode 100644 index 00000000..db06b522 --- /dev/null +++ b/unittests/feed/SingleFeedBaseTests.cpp @@ -0,0 +1,131 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/FeedBaseTest.h" +#include "feed/impl/SingleFeedBase.h" +#include "util/Fixtures.h" +#include "util/MockPrometheus.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/config/Config.h" +#include "util/prometheus/Gauge.h" +#include "web/interface/ConnectionBase.h" + +#include +#include +#include + +#include + +constexpr static auto FEED = R"({"test":"test"})"; + +using namespace feed::impl; +using namespace util::prometheus; + +struct FeedBaseMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest { +protected: + util::TagDecoratorFactory tagDecoratorFactory{util::Config{}}; + std::shared_ptr sessionPtr; + std::shared_ptr testFeedPtr; + + void + SetUp() override + { + SyncAsioContextTest::SetUp(); + testFeedPtr = std::make_shared(ctx, "testFeed"); + sessionPtr = std::make_shared(tagDecoratorFactory); + } + void + TearDown() override + { + sessionPtr.reset(); + testFeedPtr.reset(); + SyncAsioContextTest::TearDown(); + } +}; + +TEST_F(FeedBaseMockPrometheusTest, subUnsub) +{ + auto& counter = makeMock("subscriptions_current_number", "{stream=\"testFeed\"}"); + EXPECT_CALL(counter, add(1)); + EXPECT_CALL(counter, add(-1)); + + testFeedPtr->sub(sessionPtr); + testFeedPtr->unsub(sessionPtr); +} + +TEST_F(FeedBaseMockPrometheusTest, AutoUnsub) +{ + auto& counter = makeMock("subscriptions_current_number", "{stream=\"testFeed\"}"); + EXPECT_CALL(counter, add(1)); + EXPECT_CALL(counter, add(-1)); + + testFeedPtr->sub(sessionPtr); + sessionPtr.reset(); +} + +class NamedSingleFeedTest : public SingleFeedBase { +public: + NamedSingleFeedTest(boost::asio::io_context& ioContext) : SingleFeedBase(ioContext, "forTest") + { + } +}; + +using SingleFeedBaseTest = FeedBaseTest; + +TEST_F(SingleFeedBaseTest, Test) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + testFeedPtr->pub(FEED); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage(), FEED); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); + cleanReceivedFeed(); + testFeedPtr->pub(FEED); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(SingleFeedBaseTest, TestAutoDisconnect) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + testFeedPtr->pub(FEED); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage(), FEED); + sessionPtr.reset(); + EXPECT_EQ(testFeedPtr->count(), 0); +} + +TEST_F(SingleFeedBaseTest, RepeatSub) +{ + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + testFeedPtr->sub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 1); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->count(), 0); +} diff --git a/unittests/feed/SubscriptionManagerTests.cpp b/unittests/feed/SubscriptionManagerTests.cpp new file mode 100644 index 00000000..8c580498 --- /dev/null +++ b/unittests/feed/SubscriptionManagerTests.cpp @@ -0,0 +1,484 @@ +//------------------------------------------------------------------------------ +/* + 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 "data/Types.h" +#include "feed/SubscriptionManager.h" +#include "util/Fixtures.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/TestObject.h" +#include "util/config/Config.h" +#include "web/interface/ConnectionBase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; + +namespace json = boost::json; +using namespace feed; +using namespace feed::impl; + +class SubscriptionManagerTest : public MockBackendTest, public SyncAsioContextTest { +protected: + util::Config cfg; + std::shared_ptr SubscriptionManagerPtr; + util::TagDecoratorFactory tagDecoratorFactory{cfg}; + std::shared_ptr session; + + void + SetUp() override + { + MockBackendTest::SetUp(); + SyncAsioContextTest::SetUp(); + SubscriptionManagerPtr = std::make_shared(ctx, backend); + session = std::make_shared(tagDecoratorFactory); + } + + void + TearDown() override + { + session.reset(); + SubscriptionManagerPtr.reset(); + SyncAsioContextTest::TearDown(); + MockBackendTest::TearDown(); + } + + std::string const& + receivedFeedMessage() const + { + auto const mockSession = dynamic_cast(session.get()); + [&] { ASSERT_NE(mockSession, nullptr); }(); + return mockSession->message; + } + + void + cleanReceivedFeed() + { + auto mockSession = dynamic_cast(session.get()); + [&] { ASSERT_NE(mockSession, nullptr); }(); + mockSession->message.clear(); + } +}; + +TEST_F(SubscriptionManagerTest, MultipleThreadCtx) +{ + std::optional work_; + work_.emplace(ctx); // guard the context + + std::vector workers; + workers.reserve(2); + for (int i = 0; i < 2; ++i) + workers.emplace_back([this]() { ctx.run(); }); + + SubscriptionManagerPtr->subManifest(session); + SubscriptionManagerPtr->subValidation(session); + + SubscriptionManagerPtr->forwardManifest(json::parse(R"({"manifest":"test"})").get_object()); + SubscriptionManagerPtr->forwardValidation(json::parse(R"({"validation":"test"})").get_object()); + + auto retry = 5; + while (--retry != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + if (receivedFeedMessage() == R"({"manifest":"test"}{"validation":"test"})" || + receivedFeedMessage() == R"({"validation":"test"}{"manifest":"test"})") + break; + } + EXPECT_TRUE(retry != 0) << "receivedFeedMessage() = " << receivedFeedMessage(); + + session.reset(); + work_.reset(); + + for (auto& worker : workers) + worker.join(); + SubscriptionManagerPtr.reset(); +} + +TEST_F(SubscriptionManagerTest, MultipleThreadCtxSessionDieEarly) +{ + boost::asio::executor_work_guard work_ = boost::asio::make_work_guard(ctx); + + std::vector workers; + workers.reserve(2); + for (int i = 0; i < 2; ++i) + workers.emplace_back([this]() { ctx.run(); }); + + SubscriptionManagerPtr->subManifest(session); + SubscriptionManagerPtr->subValidation(session); + + SubscriptionManagerPtr->forwardManifest(json::parse(R"({"manifest":"test"})").get_object()); + SubscriptionManagerPtr->forwardValidation(json::parse(R"({"validation":"test"})").get_object()); + + session.reset(); + + work_.reset(); + for (auto& worker : workers) + worker.join(); + // SubscriptionManager's pub job is running in thread pool, so we let thread pool run out of work, otherwise + // SubscriptionManager will die before the job is called + SubscriptionManagerPtr.reset(); +} + +TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber) +{ + constexpr static auto ReportReturn = + R"({ + "ledger":0, + "transactions":2, + "transactions_proposed":2, + "manifests":2, + "validations":2, + "account":2, + "accounts_proposed":2, + "books":2, + "book_changes":2 + })"; + std::shared_ptr const session1 = std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = std::make_shared(tagDecoratorFactory); + SubscriptionManagerPtr->subBookChanges(session1); + SubscriptionManagerPtr->subBookChanges(session2); + SubscriptionManagerPtr->subManifest(session1); + SubscriptionManagerPtr->subManifest(session2); + SubscriptionManagerPtr->subProposedTransactions(session1); + SubscriptionManagerPtr->subProposedTransactions(session2); + SubscriptionManagerPtr->subTransactions(session1, 1); + SubscriptionManagerPtr->subTransactions(session2, 2); + SubscriptionManagerPtr->subValidation(session1); + SubscriptionManagerPtr->subValidation(session2); + auto const account = GetAccountIDWithString(ACCOUNT1); + SubscriptionManagerPtr->subAccount(account, session1, 1); + SubscriptionManagerPtr->subAccount(account, session2, 2); + SubscriptionManagerPtr->subProposedAccount(account, session1); + SubscriptionManagerPtr->subProposedAccount(account, session2); + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + SubscriptionManagerPtr->subBook(book, session1, 1); + SubscriptionManagerPtr->subBook(book, session2, 2); + EXPECT_EQ(SubscriptionManagerPtr->report(), json::parse(ReportReturn)); + + // count down when unsub manually + SubscriptionManagerPtr->unsubBookChanges(session1); + SubscriptionManagerPtr->unsubManifest(session1); + SubscriptionManagerPtr->unsubProposedTransactions(session1); + SubscriptionManagerPtr->unsubTransactions(session1); + SubscriptionManagerPtr->unsubValidation(session1); + SubscriptionManagerPtr->unsubAccount(account, session1); + SubscriptionManagerPtr->unsubProposedAccount(account, session1); + SubscriptionManagerPtr->unsubBook(book, session1); + + // try to unsub an account which is not subscribed + auto const account2 = GetAccountIDWithString(ACCOUNT2); + SubscriptionManagerPtr->unsubAccount(account2, session1); + SubscriptionManagerPtr->unsubProposedAccount(account2, session1); + auto checkResult = [](json::object reportReturn, int result) { + EXPECT_EQ(reportReturn["book_changes"], result); + EXPECT_EQ(reportReturn["validations"], result); + EXPECT_EQ(reportReturn["transactions_proposed"], result); + EXPECT_EQ(reportReturn["transactions"], result); + EXPECT_EQ(reportReturn["manifests"], result); + EXPECT_EQ(reportReturn["accounts_proposed"], result); + EXPECT_EQ(reportReturn["account"], result); + EXPECT_EQ(reportReturn["books"], result); + }; + checkResult(SubscriptionManagerPtr->report(), 1); + + // count down when session disconnect + session2.reset(); + checkResult(SubscriptionManagerPtr->report(), 0); +} + +TEST_F(SubscriptionManagerTest, ManifestTest) +{ + SubscriptionManagerPtr->subManifest(session); + constexpr static auto dummyManifest = R"({"manifest":"test"})"; + SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object()); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyManifest)); + + cleanReceivedFeed(); + SubscriptionManagerPtr->unsubManifest(session); + SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object()); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(SubscriptionManagerTest, ValidationTest) +{ + SubscriptionManagerPtr->subValidation(session); + constexpr static auto dummyManifest = R"({"validation":"test"})"; + SubscriptionManagerPtr->forwardValidation(json::parse(dummyManifest).get_object()); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyManifest)); + cleanReceivedFeed(); + SubscriptionManagerPtr->unsubValidation(session); + SubscriptionManagerPtr->forwardValidation(json::parse(dummyManifest).get_object()); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(SubscriptionManagerTest, BookChangesTest) +{ + SubscriptionManagerPtr->subBookChanges(session); + EXPECT_EQ(SubscriptionManagerPtr->report()["book_changes"], 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32); + auto transactions = std::vector{}; + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STObject const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + transactions.push_back(trans1); + + SubscriptionManagerPtr->pubBookChanges(ledgerinfo, transactions); + constexpr static auto bookChangePublish = + R"({ + "type":"bookChanges", + "ledger_index":32, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "changes": + [ + { + "currency_a":"XRP_drops", + "currency_b":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD/0158415500000000C1F76FF6ECB0BAC600000000", + "volume_a":"2", + "volume_b":"2", + "high":"-1", + "low":"-1", + "open":"-1", + "close":"-1" + } + ] + })"; + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(bookChangePublish)); + + SubscriptionManagerPtr->unsubBookChanges(session); + EXPECT_EQ(SubscriptionManagerPtr->report()["book_changes"], 0); +} + +TEST_F(SubscriptionManagerTest, LedgerTest) +{ + backend->setRange(10, 30); + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerinfo)); + + auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); + EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob)); + // check the function response + // Information about the ledgers on hand and current fee schedule. This + // includes the same fields as a ledger stream message, except that it omits + // the type and txn_count fields + constexpr static auto LedgerResponse = + R"({ + "validated_ledgers":"10-30", + "ledger_index":30, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_base":1, + "reserve_base":3, + "reserve_inc":2 + })"; + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const res = SubscriptionManagerPtr->subLedger(yield, session); + // check the response + EXPECT_EQ(res, json::parse(LedgerResponse)); + }); + ctx.run(); + EXPECT_EQ(SubscriptionManagerPtr->report()["ledger"], 1); + + // test publish + auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31); + auto fee2 = ripple::Fees(); + fee2.reserve = 10; + SubscriptionManagerPtr->pubLedger(ledgerinfo2, fee2, "10-31", 8); + constexpr static auto ledgerPub = + R"({ + "type":"ledgerClosed", + "ledger_index":31, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_base":0, + "reserve_base":10, + "reserve_inc":0, + "validated_ledgers":"10-31", + "txn_count":8 + })"; + ctx.restart(); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(ledgerPub)); + + // test unsub + SubscriptionManagerPtr->unsubLedger(session); + EXPECT_EQ(SubscriptionManagerPtr->report()["ledger"], 0); +} + +TEST_F(SubscriptionManagerTest, TransactionTest) +{ + auto const issue1 = GetIssue(CURRENCY, ISSUER); + auto const account = GetAccountIDWithString(ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + SubscriptionManagerPtr->subBook(book, session, 1); + SubscriptionManagerPtr->subTransactions(session, 1); + SubscriptionManagerPtr->subAccount(account, session, 1); + EXPECT_EQ(SubscriptionManagerPtr->report()["account"], 1); + EXPECT_EQ(SubscriptionManagerPtr->report()["transactions"], 1); + EXPECT_EQ(SubscriptionManagerPtr->report()["books"], 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + + auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3); + trans1.metadata = metaObj.getSerializer().peekData(); + SubscriptionManagerPtr->pubTransaction(trans1, ledgerinfo); + + constexpr static auto OrderbookPublish = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "TakerGets":"3", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer", + "PreviousFields": + { + "TakerGets":"1", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"3" + } + } + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "close_time_iso": "2000-01-01T00:00:00Z", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + + ctx.run(); + + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(OrderbookPublish)).size() * 3); + + SubscriptionManagerPtr->unsubBook(book, session); + SubscriptionManagerPtr->unsubTransactions(session); + SubscriptionManagerPtr->unsubAccount(account, session); + EXPECT_EQ(SubscriptionManagerPtr->report()["account"], 0); + EXPECT_EQ(SubscriptionManagerPtr->report()["transactions"], 0); + EXPECT_EQ(SubscriptionManagerPtr->report()["books"], 0); +} + +TEST_F(SubscriptionManagerTest, ProposedTransactionTest) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + SubscriptionManagerPtr->subProposedAccount(account, session); + SubscriptionManagerPtr->subProposedTransactions(session); + EXPECT_EQ(SubscriptionManagerPtr->report()["accounts_proposed"], 1); + EXPECT_EQ(SubscriptionManagerPtr->report()["transactions_proposed"], 1); + + constexpr static auto dummyTransaction = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + } + })"; + + SubscriptionManagerPtr->forwardProposedTransaction(json::parse(dummyTransaction).get_object()); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(dummyTransaction)).size() * 2); + + // unsub account1 + cleanReceivedFeed(); + SubscriptionManagerPtr->unsubProposedAccount(account, session); + EXPECT_EQ(SubscriptionManagerPtr->report()["accounts_proposed"], 0); + SubscriptionManagerPtr->unsubProposedTransactions(session); + EXPECT_EQ(SubscriptionManagerPtr->report()["transactions_proposed"], 0); +} diff --git a/unittests/feed/TrackableSignalTests.cpp b/unittests/feed/TrackableSignalTests.cpp new file mode 100644 index 00000000..8a8e5ebf --- /dev/null +++ b/unittests/feed/TrackableSignalTests.cpp @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------ +/* + 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 "feed/impl/TrackableSignal.h" +#include "feed/impl/TrackableSignalMap.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/config/Config.h" +#include "web/interface/ConnectionBase.h" + +#include + +#include +#include + +using namespace testing; + +struct FeedTrackableSignalTests : Test { +protected: + util::TagDecoratorFactory tagDecoratorFactory{util::Config{}}; + std::shared_ptr sessionPtr; + + void + SetUp() override + { + sessionPtr = std::make_shared(tagDecoratorFactory); + } + + void + TearDown() override + { + sessionPtr.reset(); + } +}; + +TEST_F(FeedTrackableSignalTests, Connect) +{ + feed::impl::TrackableSignal signal; + std::string testString; + auto const slot = [&](std::string const& s) { testString += s; }; + EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot)); + EXPECT_FALSE(signal.connectTrackableSlot(sessionPtr, slot)); + + EXPECT_EQ(signal.count(), 1); + + signal.emit("test"); + EXPECT_EQ(testString, "test"); + + EXPECT_TRUE(signal.disconnect(sessionPtr.get())); + EXPECT_EQ(signal.count(), 0); + EXPECT_FALSE(signal.disconnect(sessionPtr.get())); + + testString.clear(); + signal.emit("test2"); + EXPECT_TRUE(testString.empty()); +} + +TEST_F(FeedTrackableSignalTests, AutoDisconnect) +{ + feed::impl::TrackableSignal signal; + std::string testString; + auto const slot = [&](std::string const& s) { testString += s; }; + EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot)); + EXPECT_FALSE(signal.connectTrackableSlot(sessionPtr, slot)); + + EXPECT_EQ(signal.count(), 1); + + signal.emit("test"); + EXPECT_EQ(testString, "test"); + + sessionPtr.reset(); + // track object is destroyed, but the connection is still there + EXPECT_EQ(signal.count(), 1); + + testString.clear(); + signal.emit("test2"); + EXPECT_TRUE(testString.empty()); +} + +TEST_F(FeedTrackableSignalTests, MapConnect) +{ + feed::impl::TrackableSignalMap signalMap; + std::string testString; + auto const slot = [&](std::string const& s) { testString += s; }; + EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot)); + EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test1", slot)); + EXPECT_FALSE(signalMap.connectTrackableSlot(sessionPtr, "test", slot)); + + signalMap.emit("test", "test"); + signalMap.emit("test2", "test2"); + EXPECT_EQ(testString, "test"); + + EXPECT_TRUE(signalMap.disconnect(sessionPtr.get(), "test")); + EXPECT_FALSE(signalMap.disconnect(sessionPtr.get(), "test")); + + testString.clear(); + signalMap.emit("test", "test2"); + EXPECT_TRUE(testString.empty()); + + signalMap.emit("test1", "test1"); + EXPECT_EQ(testString, "test1"); +} + +TEST_F(FeedTrackableSignalTests, MapAutoDisconnect) +{ + feed::impl::TrackableSignalMap signalMap; + std::string testString; + auto const slot = [&](std::string const& s) { testString += s; }; + EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot)); + EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test1", slot)); + EXPECT_FALSE(signalMap.connectTrackableSlot(sessionPtr, "test", slot)); + + signalMap.emit("test", "test"); + signalMap.emit("test2", "test2"); + EXPECT_EQ(testString, "test"); + + // kill trackable + sessionPtr.reset(); + + testString.clear(); + signalMap.emit("test", "test"); + EXPECT_TRUE(testString.empty()); + + signalMap.emit("test1", "test1"); + EXPECT_TRUE(testString.empty()); +} diff --git a/unittests/feed/TransactionFeedTests.cpp b/unittests/feed/TransactionFeedTests.cpp new file mode 100644 index 00000000..569999fa --- /dev/null +++ b/unittests/feed/TransactionFeedTests.cpp @@ -0,0 +1,1044 @@ +//------------------------------------------------------------------------------ +/* + 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 "data/Types.h" +#include "feed/FeedBaseTest.h" +#include "feed/impl/TransactionFeed.h" +#include "util/Fixtures.h" +#include "util/MockPrometheus.h" +#include "util/MockWsBase.h" +#include "util/Taggable.h" +#include "util/TestObject.h" +#include "util/config/Config.h" +#include "util/prometheus/Gauge.h" +#include "web/interface/ConnectionBase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto TXNID = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; + +constexpr static auto TRAN_V1 = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance":"110" + }, + "LedgerEntryType":"AccountRoot" + } + }, + { + "ModifiedNode": + { + "FinalFields": + { + "Account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance":"30" + }, + "LedgerEntryType":"AccountRoot" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + +constexpr static auto TRAN_V2 = + R"({ + "tx_json": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode":{ + "FinalFields":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Balance":"110" + }, + "LedgerEntryType":"AccountRoot" + } + }, + { + "ModifiedNode":{ + "FinalFields":{ + "Account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Balance":"30" + }, + "LedgerEntryType":"AccountRoot" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + +using namespace feed::impl; +using namespace util::prometheus; +namespace json = boost::json; + +using FeedTransactionTest = FeedBaseTest; + +TEST_F(FeedTransactionTest, SubTransactionV1) +{ + testFeedPtr->sub(sessionPtr, 1); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V1)); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubTransactionV2) +{ + testFeedPtr->sub(sessionPtr, 2); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubAccountV1) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 1); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V1)); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubAccountV2) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubBothTransactionAndAccount) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 2); + testFeedPtr->sub(sessionPtr, 2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.run(); + + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(TRAN_V2)).size() * 2); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(TRAN_V2)).size() * 2); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubBookV1) +{ + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + testFeedPtr->sub(book, sessionPtr, 1); + EXPECT_EQ(testFeedPtr->bookSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + + auto metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3); + trans1.metadata = metaObj.getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + constexpr static auto OrderbookPublish = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "TakerGets":"3", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer", + "PreviousFields":{ + "TakerGets":"1", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"3" + } + } + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "close_time_iso": "2000-01-01T00:00:00Z", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(OrderbookPublish)); + + cleanReceivedFeed(); + + // trigger by offer cancel meta data + metaObj = CreateMetaDataForCancelOffer(CURRENCY, ISSUER, 22, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + constexpr static auto OrderbookCancelPublish = + R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes": + [ + { + "DeletedNode": + { + "FinalFields": + { + "TakerGets":"3", + "TakerPays":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "close_time_iso": "2000-01-01T00:00:00Z", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + ctx.restart(); + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(OrderbookCancelPublish)); + + // trigger by offer create meta data + constexpr static auto OrderbookCreatePublish = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "CreatedNode": + { + "NewFields":{ + "TakerGets":"3", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "close_time_iso": "2000-01-01T00:00:00Z", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + cleanReceivedFeed(); + ctx.restart(); + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(OrderbookCreatePublish)); + + testFeedPtr->unsub(book, sessionPtr); + EXPECT_EQ(testFeedPtr->bookSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubBookV2) +{ + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + testFeedPtr->sub(book, sessionPtr, 2); + EXPECT_EQ(testFeedPtr->bookSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + + auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3); + trans1.metadata = metaObj.getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + constexpr static auto OrderbookPublish = + R"({ + "tx_json": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "DeliverMax":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "date":0 + }, + "meta": + { + "AffectedNodes": + [ + { + "ModifiedNode": + { + "FinalFields": + { + "TakerGets":"3", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer", + "PreviousFields": + { + "TakerGets":"1", + "TakerPays": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"3" + } + } + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "close_time_iso": "2000-01-01T00:00:00Z", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(OrderbookPublish)); + + testFeedPtr->unsub(book, sessionPtr); + EXPECT_EQ(testFeedPtr->bookSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 2); + + auto const account2 = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(account2, sessionPtr, 2); + + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + cleanReceivedFeed(); + testFeedPtr->unsub(account2, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion) +{ + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 1); + + auto const account2 = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(account2, sessionPtr, 2); + + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V2)); + + testFeedPtr->unsub(account2, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + cleanReceivedFeed(); + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion) +{ + // sub version 1 first + testFeedPtr->sub(sessionPtr, 1); + // sub version 2 later + testFeedPtr->sub(sessionPtr, 2); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData(); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_V1)); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + cleanReceivedFeed(); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + ctx.restart(); + ctx.run(); + EXPECT_TRUE(receivedFeedMessage().empty()); +} + +TEST_F(FeedTransactionTest, SubRepeat) +{ + auto const session2 = std::make_shared(tagDecoratorFactory); + + testFeedPtr->sub(sessionPtr, 1); + testFeedPtr->sub(session2, 1); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 2); + + testFeedPtr->sub(sessionPtr, 1); + testFeedPtr->sub(session2, 1); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 2); + + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 1); + testFeedPtr->unsub(session2); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + testFeedPtr->unsub(sessionPtr); + EXPECT_EQ(testFeedPtr->transactionSubCount(), 0); + + auto const account = GetAccountIDWithString(ACCOUNT1); + auto const account2 = GetAccountIDWithString(ACCOUNT2); + testFeedPtr->sub(account, sessionPtr, 1); + testFeedPtr->sub(account2, session2, 1); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + testFeedPtr->sub(account, sessionPtr, 1); + testFeedPtr->sub(account2, session2, 1); + EXPECT_EQ(testFeedPtr->accountSubCount(), 2); + + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 1); + testFeedPtr->unsub(account2, session2); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + testFeedPtr->unsub(account, sessionPtr); + EXPECT_EQ(testFeedPtr->accountSubCount(), 0); + + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + testFeedPtr->sub(book, sessionPtr, 1); + EXPECT_EQ(testFeedPtr->bookSubCount(), 1); + testFeedPtr->sub(book, session2, 1); + EXPECT_EQ(testFeedPtr->bookSubCount(), 2); + + testFeedPtr->unsub(book, sessionPtr); + EXPECT_EQ(testFeedPtr->bookSubCount(), 1); + testFeedPtr->unsub(book, session2); + EXPECT_EQ(testFeedPtr->bookSubCount(), 0); + testFeedPtr->unsub(book, sessionPtr); + EXPECT_EQ(testFeedPtr->bookSubCount(), 0); +} + +TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund) +{ + testFeedPtr->sub(sessionPtr, 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray const metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, 0); + auto const issue2 = GetIssue(CURRENCY, ISSUER); + line.setFieldAmount(ripple::sfBalance, ripple::STAmount(issue2, 100)); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + auto const issueAccount = GetAccountIDWithString(ISSUER); + auto const kk = ripple::keylet::account(issueAccount).key; + ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_)) + .WillByDefault(testing::Return(line.getSerializer().peekData())); + ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, 0, 1, 10, 2, TXNID, 3); + ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_)) + .WillByDefault(testing::Return(accountRoot.getSerializer().peekData())); + + testFeedPtr->pub(trans1, ledgerinfo, backend); + constexpr static auto TransactionForOwnerFund = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TakerGets": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + }, + "TakerPays":"3", + "TransactionType":"OfferCreate", + "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", + "date":0, + "owner_funds":"100" + }, + "meta": + { + "AffectedNodes":[], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "close_time_iso": "2000-01-01T00:00:00Z", + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TransactionForOwnerFund)); +} + +constexpr static auto TRAN_FROZEN = + R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TakerGets": + { + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + }, + "TakerPays":"3", + "TransactionType":"OfferCreate", + "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", + "date":0, + "owner_funds":"0" + }, + "meta":{ + "AffectedNodes":[], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "close_time_iso": "2000-01-01T00:00:00Z", + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + +TEST_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine) +{ + testFeedPtr->sub(sessionPtr, 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray const metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); + line.setFieldAmount(ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + auto const issueAccount = GetAccountIDWithString(ISSUER); + auto const kk = ripple::keylet::account(issueAccount).key; + ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_)) + .WillByDefault(testing::Return(line.getSerializer().peekData())); + ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, 0, 1, 10, 2, TXNID, 3); + ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_)) + .WillByDefault(testing::Return(accountRoot.getSerializer().peekData())); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_FROZEN)); +} + +TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen) +{ + testFeedPtr->sub(sessionPtr, 1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray const metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); + auto const issueAccount = GetAccountIDWithString(ISSUER); + line.setFieldAmount(ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); + auto const kk = ripple::keylet::account(issueAccount).key; + ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_)) + .WillByDefault(testing::Return(line.getSerializer().peekData())); + ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, ripple::lsfGlobalFreeze, 1, 10, 2, TXNID, 3); + ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_)) + .WillByDefault(testing::Return(accountRoot.getSerializer().peekData())); + testFeedPtr->pub(trans1, ledgerinfo, backend); + + ctx.run(); + EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(TRAN_FROZEN)); +} + +struct TransactionFeedMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest { +protected: + util::TagDecoratorFactory tagDecoratorFactory{util::Config{}}; + std::shared_ptr sessionPtr; + std::shared_ptr testFeedPtr; + + void + SetUp() override + { + SyncAsioContextTest::SetUp(); + testFeedPtr = std::make_shared(ctx); + sessionPtr = std::make_shared(tagDecoratorFactory); + } + void + TearDown() override + { + sessionPtr.reset(); + testFeedPtr.reset(); + SyncAsioContextTest::TearDown(); + } +}; + +TEST_F(TransactionFeedMockPrometheusTest, subUnsub) +{ + auto& counterTx = makeMock("subscriptions_current_number", "{stream=\"tx\"}"); + auto& counterAccount = makeMock("subscriptions_current_number", "{stream=\"account\"}"); + auto& counterBook = makeMock("subscriptions_current_number", "{stream=\"book\"}"); + + EXPECT_CALL(counterTx, add(1)); + EXPECT_CALL(counterTx, add(-1)); + EXPECT_CALL(counterAccount, add(1)); + EXPECT_CALL(counterAccount, add(-1)); + EXPECT_CALL(counterBook, add(1)); + EXPECT_CALL(counterBook, add(-1)); + + testFeedPtr->sub(sessionPtr, 1); + testFeedPtr->unsub(sessionPtr); + + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 1); + testFeedPtr->unsub(account, sessionPtr); + + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + testFeedPtr->sub(book, sessionPtr, 1); + testFeedPtr->unsub(book, sessionPtr); +} + +TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect) +{ + auto& counterTx = makeMock("subscriptions_current_number", "{stream=\"tx\"}"); + auto& counterAccount = makeMock("subscriptions_current_number", "{stream=\"account\"}"); + auto& counterBook = makeMock("subscriptions_current_number", "{stream=\"book\"}"); + + EXPECT_CALL(counterTx, add(1)); + EXPECT_CALL(counterTx, add(-1)); + EXPECT_CALL(counterAccount, add(1)); + EXPECT_CALL(counterAccount, add(-1)); + EXPECT_CALL(counterBook, add(1)); + EXPECT_CALL(counterBook, add(-1)); + + testFeedPtr->sub(sessionPtr, 1); + + auto const account = GetAccountIDWithString(ACCOUNT1); + testFeedPtr->sub(account, sessionPtr, 1); + + auto const issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book const book{ripple::xrpIssue(), issue1}; + testFeedPtr->sub(book, sessionPtr, 1); + + sessionPtr.reset(); +} diff --git a/unittests/rpc/handlers/SubscribeTests.cpp b/unittests/rpc/handlers/SubscribeTests.cpp index 863ba3ce..a67c078c 100644 --- a/unittests/rpc/handlers/SubscribeTests.cpp +++ b/unittests/rpc/handlers/SubscribeTests.cpp @@ -70,9 +70,9 @@ protected: SetUp() override { HandlerBaseTest::SetUp(); - util::Config const cfg; - subManager_ = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend); - util::TagDecoratorFactory const tagDecoratorFactory{cfg}; + + subManager_ = std::make_shared(ctx, backend); + util::TagDecoratorFactory const tagDecoratorFactory{util::Config{}}; session_ = std::make_shared(tagDecoratorFactory); } void diff --git a/unittests/web/RPCServerHandlerTests.cpp b/unittests/web/RPCServerHandlerTests.cpp index 45a8d5c8..05275dd5 100644 --- a/unittests/web/RPCServerHandlerTests.cpp +++ b/unittests/web/RPCServerHandlerTests.cpp @@ -16,7 +16,6 @@ */ //============================================================================== -#include "feed/SubscriptionManager.h" #include "rpc/Errors.h" #include "util/Fixtures.h" #include "util/MockETLService.h" @@ -66,7 +65,7 @@ struct MockWsBase : public web::ConnectionBase { } }; -class WebRPCServerHandlerTest : public MockBackendTest { +class WebRPCServerHandlerTest : public MockBackendTest, public SyncAsioContextTest { protected: void SetUp() override @@ -76,11 +75,8 @@ protected: etl = std::make_shared(); rpcEngine = std::make_shared(); tagFactory = std::make_shared(cfg); - subManager = std::make_shared(cfg, backend); session = std::make_shared(*tagFactory); - handler = std::make_shared>( - cfg, backend, rpcEngine, etl, subManager - ); + handler = std::make_shared>(cfg, backend, rpcEngine, etl); } void @@ -91,7 +87,6 @@ protected: std::shared_ptr rpcEngine; std::shared_ptr etl; - std::shared_ptr subManager; std::shared_ptr tagFactory; std::shared_ptr> handler; std::shared_ptr session; @@ -703,9 +698,8 @@ TEST_F(WebRPCServerHandlerTest, WsTooBusy) session->upgraded = true; auto localRpcEngine = std::make_shared(); - auto localHandler = std::make_shared>( - cfg, backend, localRpcEngine, etl, subManager - ); + auto localHandler = + std::make_shared>(cfg, backend, localRpcEngine, etl); static auto constexpr request = R"({ "command": "server_info", "id": 99 @@ -732,9 +726,8 @@ TEST_F(WebRPCServerHandlerTest, WsTooBusy) TEST_F(WebRPCServerHandlerTest, HTTPTooBusy) { auto localRpcEngine = std::make_shared(); - auto localHandler = std::make_shared>( - cfg, backend, localRpcEngine, etl, subManager - ); + auto localHandler = + std::make_shared>(cfg, backend, localRpcEngine, etl); static auto constexpr request = R"({ "method": "server_info", "params": [{}]