New subscription manager (#1071)

Fix #886
This commit is contained in:
cyan317
2024-01-08 14:45:57 +00:00
committed by GitHub
parent 07bd4b0760
commit eb1831c489
37 changed files with 4427 additions and 2098 deletions

View File

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

View File

@@ -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 <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/basics/base_uint.h>
#include <ripple/basics/chrono.h>
#include <ripple/basics/strHex.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/LedgerFormats.h>
#include <ripple/protocol/LedgerHeader.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STAmount.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFormats.h>
#include <ripple/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
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<std::string> 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<data::TransactionAndMetadata> 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<ripple::Fees> 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<std::string>(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<std::string>(boost::json::serialize(pubObj));
txSubscribers_.publish(pubMsg);
auto accounts = meta->getAffectedAccounts();
for (auto const& account : accounts)
accountSubscribers_.publish(pubMsg, account);
std::unordered_set<ripple::Book> 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<ripple::STObject const*>(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<data::TransactionAndMetadata> 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<std::string>(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<std::string>(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<std::string>(boost::json::serialize(response));
manifestSubscribers_.publish(pubMsg);
}
void
SubscriptionManager::forwardValidation(boost::json::object const& response)
{
auto pubMsg = std::make_shared<std::string>(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 <typename Key>
void
SubscriptionManager::subscribeHelper(
SessionPtrType const& session,
Key const& k,
SubscriptionMap<Key>& 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

View File

@@ -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 <fmt/format.h>
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/LedgerHeader.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <thread>
#include <vector>
/**
* @brief This namespace deals with subscriptions.
*/
namespace feed {
using SessionPtrType = std::shared_ptr<web::ConnectionBase>;
/**
* @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 <class T>
inline void
sendToSubscribers(std::shared_ptr<std::string> 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 <class T>
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 <class T>
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<boost::asio::io_context::executor_type> strand_;
std::unordered_set<SessionPtrType> 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<std::string> 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 Key>
class SubscriptionMap {
using SubscribersType = std::set<SessionPtrType>;
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::unordered_map<Key, SubscribersType> 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<std::string> 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<std::thread> workers_;
boost::asio::io_context ioc_;
std::optional<boost::asio::io_context::work> work_;
Subscription ledgerSubscribers_;
Subscription txSubscribers_;
Subscription txProposedSubscribers_;
Subscription manifestSubscribers_;
Subscription validationsSubscribers_;
Subscription bookChangesSubscribers_;
SubscriptionMap<ripple::AccountID> accountSubscribers_;
SubscriptionMap<ripple::AccountID> accountProposedSubscribers_;
SubscriptionMap<ripple::Book> bookSubscribers_;
std::reference_wrapper<boost::asio::io_context> ioContext_;
std::shared_ptr<data::BackendInterface const> 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<SubscriptionManager>
make_SubscriptionManager(util::Config const& config, std::shared_ptr<data::BackendInterface const> const& backend)
{
auto numThreads = config.valueOr<uint64_t>("subscription_workers", 1);
return std::make_shared<SubscriptionManager>(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<data::BackendInterface const> 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<data::BackendInterface const> 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<data::TransactionAndMetadata> 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<data::TransactionAndMetadata> 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(SessionPtrType const)>;
void
subscribeHelper(SessionPtrType const& session, Subscription& subs, CleanupFunction&& func);
template <typename Key>
void
subscribeHelper(SessionPtrType const& session, Key const& k, SubscriptionMap<Key>& 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<SessionPtrType, std::vector<CleanupFunction>> 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> subscriptionManager_;
util::Logger logger_{"Subscriptions"};
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_ =
boost::asio::make_work_guard(ioContext_);
std::vector<std::thread> workers_;
public:
SubscriptionManagerRunner(util::Config const& config, std::shared_ptr<data::BackendInterface> const& backend)
: subscriptionManager_(std::make_shared<SubscriptionManager>(ioContext_, backend))
{
auto numThreads = config.valueOr<uint64_t>("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<SubscriptionManager>
getManager()
{
return subscriptionManager_;
}
~SubscriptionManagerRunner()
{
work_.reset();
for (auto& worker : workers_)
worker.join();
}
};
} // namespace feed

31
src/feed/Types.h Normal file
View File

@@ -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 <memory>
namespace feed {
using Subscriber = web::ConnectionBase;
using SubscriberPtr = Subscriber*;
using SubscriberSharedPtr = std::shared_ptr<Subscriber>;
} // namespace feed

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/protocol/LedgerHeader.h>
#include <vector>
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<data::TransactionAndMetadata> const& transactions) const
{
SingleFeedBase::pub(boost::json::serialize(rpc::computeBookChanges(lgrInfo, transactions)));
}
};
} // namespace feed::impl

View File

@@ -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 <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
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

View File

@@ -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 <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/LedgerHeader.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
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<data::BackendInterface const> 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

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/LedgerHeader.h>
#include <cstdint>
#include <memory>
#include <string>
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<data::BackendInterface const> 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

View File

@@ -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 <boost/asio/post.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/protocol/AccountID.h>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
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<std::string> 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<std::string> 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<std::string>(boost::json::serialize(receivedTxJson));
auto const transaction = receivedTxJson.at("transaction").as_object();
auto const accounts = rpc::getAccountsFromTransaction(transaction);
auto affectedAccounts = std::unordered_set<ripple::AccountID>(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

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <boost/json/object.hpp>
#include <fmt/core.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/LedgerHeader.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_set>
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<SubscriberPtr>
notified_; // Used by slots to prevent double notifications if tx contains multiple subscribed accounts
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::reference_wrapper<util::prometheus::GaugeInt> subAllCount_;
std::reference_wrapper<util::prometheus::GaugeInt> subAccountCount_;
TrackableSignalMap<ripple::AccountID, Subscriber, std::shared_ptr<std::string>> accountSignal_;
TrackableSignal<Subscriber, std::shared_ptr<std::string>> 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

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/strand.hpp>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
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<std::string> 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::string>(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

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
namespace feed::impl {
/**
* @brief Base class for single feed.
*/
class SingleFeedBase {
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::reference_wrapper<util::prometheus::GaugeInt> subCount_;
TrackableSignal<Subscriber, std::shared_ptr<std::string> 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

View File

@@ -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 <boost/signals2.hpp>
#include <boost/signals2/connection.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>
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 <typename Session, typename... Args>
class TrackableSignal {
using ConnectionPtr = Session*;
using ConnectionSharedPtr = std::shared_ptr<Session>;
// 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<ConnectionPtr, boost::signals2::connection> connections_;
mutable std::mutex mutex_;
using SignalType = boost::signals2::signal<void(Args...)>;
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<void(Args...)> 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

View File

@@ -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 <boost/signals2.hpp>
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>
namespace feed::impl {
template <typename T>
concept Hashable = requires(T a) {
{
std::hash<T>{}(a)
} -> std::convertible_to<std::size_t>;
};
/**
* @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 <Hashable Key, typename Session, typename... Args>
class TrackableSignalMap {
using ConnectionPtr = Session*;
using ConnectionSharedPtr = std::shared_ptr<Session>;
mutable std::mutex mutex_;
std::unordered_map<Key, TrackableSignal<Session, Args...>> 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<void(Args...)> 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

View File

@@ -0,0 +1,303 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "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 <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <ripple/basics/chrono.h>
#include <ripple/basics/strHex.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/LedgerFormats.h>
#include <ripple/protocol/LedgerHeader.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFormats.h>
#include <ripple/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
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<data::BackendInterface const> const& backend
)
{
auto [tx, meta] = rpc::deserializeTxPlusMeta(txMeta, lgrInfo.seq);
std::optional<ripple::STAmount> 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<std::string>(boost::json::serialize(genJsonByVersion(1u))),
std::make_shared<std::string>(boost::json::serialize(genJsonByVersion(2u)))
};
auto const affectedAccountsFlat = meta->getAffectedAccounts();
auto affectedAccounts =
std::unordered_set<ripple::AccountID>(affectedAccountsFlat.cbegin(), affectedAccountsFlat.cend());
std::unordered_set<ripple::Book> 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<ripple::STObject const*>(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

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <fmt/core.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/LedgerHeader.h>
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_set>
namespace feed::impl {
class TransactionFeed {
// Hold two versions of transaction messages
using AllVersionTransactionsType = std::array<std::shared_ptr<std::string>, 2>;
struct TransactionSlot {
std::reference_wrapper<TransactionFeed> feed;
std::weak_ptr<Subscriber> connectionWeakPtr;
TransactionSlot(TransactionFeed& feed, SubscriberSharedPtr const& connection)
: feed(feed), connectionWeakPtr(connection)
{
}
void
operator()(AllVersionTransactionsType const& allVersionMsgs) const;
};
util::Logger logger_{"Subscriptions"};
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::reference_wrapper<util::prometheus::GaugeInt> subAllCount_;
std::reference_wrapper<util::prometheus::GaugeInt> subAccountCount_;
std::reference_wrapper<util::prometheus::GaugeInt> subBookCount_;
TrackableSignalMap<ripple::AccountID, Subscriber, AllVersionTransactionsType const&> accountSignal_;
TrackableSignalMap<ripple::Book, Subscriber, AllVersionTransactionsType const&> bookSignal_;
TrackableSignal<Subscriber, AllVersionTransactionsType const&> signal_;
std::unordered_set<SubscriberPtr>
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<data::BackendInterface const> 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

41
src/feed/impl/Util.h Normal file
View File

@@ -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 <fmt/core.h>
#include <string>
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

View File

@@ -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<rpc::detail::ProductionHandlerProvider const>(
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<web::RPCServerHandler<rpc::RPCEngine, etl::ETLService>>(
config, backend, rpcEngine, etl, subscriptions
);
auto handler =
std::make_shared<web::RPCServerHandler<rpc::RPCEngine, etl::ETLService>>(config, backend, rpcEngine, etl);
auto ctx = parseCerts(config);
auto const ctxRef = ctx ? std::optional<std::reference_wrapper<ssl::context>>{ctx.value()} : std::nullopt;
auto const httpServer = web::make_HttpServer(config, ioc, ctxRef, dosGuard, handler);

View File

@@ -64,7 +64,6 @@ class RPCEngine {
util::Logger log_{"RPC"};
std::shared_ptr<BackendInterface> backend_;
std::shared_ptr<feed::SubscriptionManager> subscriptions_;
std::shared_ptr<etl::LoadBalancer> balancer_;
std::reference_wrapper<web::DOSGuard const> dosGuard_;
std::reference_wrapper<WorkQueue> workQueue_;
@@ -77,7 +76,6 @@ class RPCEngine {
public:
RPCEngine(
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<feed::SubscriptionManager> const& subscriptions,
std::shared_ptr<etl::LoadBalancer> const& balancer,
web::DOSGuard const& dosGuard,
WorkQueue& workQueue,
@@ -85,7 +83,6 @@ public:
std::shared_ptr<HandlerProvider const> 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<RPCEngine>
make_RPCEngine(
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<feed::SubscriptionManager> const& subscriptions,
std::shared_ptr<etl::LoadBalancer> const& balancer,
web::DOSGuard const& dosGuard,
WorkQueue& workQueue,
@@ -106,9 +102,7 @@ public:
std::shared_ptr<HandlerProvider const> const& handlerProvider
)
{
return std::make_shared<RPCEngine>(
backend, subscriptions, balancer, dosGuard, workQueue, counters, handlerProvider
);
return std::make_shared<RPCEngine>(backend, balancer, dosGuard, workQueue, counters, handlerProvider);
}
/**

View File

@@ -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 <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <ripple/beast/utility/Zero.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace 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<std::string> const& streams,
std::shared_ptr<web::ConnectionBase> const& session
std::shared_ptr<web::ConnectionBase> 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<std::string> const& accounts, std::shared_ptr<web::ConnectionBase> const& session)
const
subscribeToAccounts(
std::vector<std::string> const& accounts,
std::shared_ptr<web::ConnectionBase> 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<OrderBook> const& books,
std::shared_ptr<web::ConnectionBase> 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);
}
}

View File

@@ -64,8 +64,6 @@ class RPCServerHandler {
std::shared_ptr<BackendInterface const> const backend_;
std::shared_ptr<RPCEngineType> const rpcEngine_;
std::shared_ptr<ETLType const> const etl_;
// subscription manager holds the shared_ptr of this class
std::weak_ptr<feed::SubscriptionManager> 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<BackendInterface const> const& backend,
std::shared_ptr<RPCEngineType> const& rpcEngine,
std::shared_ptr<ETLType const> const& etl,
std::shared_ptr<feed::SubscriptionManager> const& subscriptions
std::shared_ptr<ETLType const> 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<web::ConnectionBase> const& connection)
{
if (auto manager = subscriptions_.lock(); manager)
manager->cleanup(connection);
}
private:
void
handleRequest(

View File

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

View File

@@ -37,10 +37,6 @@ concept SomeServerHandler =
{
handler(req, ws)
};
// the callback when there is an error
{
handler(ec, ws)
};
};
} // namespace web

View File

@@ -22,6 +22,7 @@
#include "util/Taggable.h"
#include <boost/beast/http.hpp>
#include <boost/signals2.hpp>
#include <utility>
@@ -42,6 +43,8 @@ public:
std::string const clientIp;
bool upgraded = false;
bool isAdmin_ = false;
boost::signals2::signal<void(ConnectionBase*)> 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.

View File

@@ -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 <boost/asio/impl/spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Issue.h>
#include <ripple/protocol/LedgerFormats.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STAmount.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/TER.h>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
#include <vector>
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<MockBackend>(cfg);
auto subManager = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend);
EXPECT_EQ(subManager->report(), json::parse(ReportReturn));
}
void
CheckSubscriberMessage(std::string out, std::shared_ptr<web::ConnectionBase> session, int retry = 10)
{
auto sessionPtr = dynamic_cast<MockSession*>(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<feed::SubscriptionManager> subManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<web::ConnectionBase> session;
void
SetUp() override
{
MockBackendTest::SetUp();
subManagerPtr = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend);
session = std::make_shared<MockSession>(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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(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<web::ConnectionBase> const sessionIdle = std::make_shared<MockSession>(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<MockSession*>(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<TransactionAndMetadata>{};
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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(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<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
subManagerPtr->subBook(book, session2);
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
subManagerPtr->pubTransaction(trans1, ledgerinfo);
CheckSubscriberMessage(OrderbookCreatePublish, session2);
}

View File

@@ -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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
sub.subscribe(session1);
sub.subscribe(session2);
ctx.run();
EXPECT_EQ(sub.count(), 2);
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
MockSession* p1 = dynamic_cast<MockSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->message, "message");
MockSession* p2 = dynamic_cast<MockSession*>(session2.get());
ASSERT_NE(p2, nullptr);
EXPECT_EQ(p2->message, "message");
sub.unsubscribe(session1);
ctx.restart();
ctx.run();
sub.publish(std::make_shared<std::string>("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<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
sub.subscribe(session1);
ctx.run();
EXPECT_EQ(sub.count(), 1);
// trigger dead
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(session1->dead(), true);
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(sub.count(), 0);
}
struct SubscriptionMockPrometheusTest : WithMockPrometheus, SubscriptionTestBase, SyncAsioContextTest {
Subscription sub{ctx, "test"};
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
};
TEST_F(SubscriptionMockPrometheusTest, subscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, add(1));
sub.subscribe(session);
ctx.run();
}
TEST_F(SubscriptionMockPrometheusTest, unsubscribe)
{
auto& counter = makeMock<GaugeInt>("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<MockDeadSession>(tagDecoratorFactory);
auto& counter = makeMock<GaugeInt>("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<std::string>("message"));
sub.publish(std::make_shared<std::string>("message")); // Dead session is detected only after failed send
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMockPrometheusTest, count)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, value());
sub.count();
}
struct SubscriptionMapTest : SubscriptionTest {
SubscriptionMap<std::string> subMap{ctx, "test"};
};
TEST_F(SubscriptionMapTest, SubscriptionMapCount)
{
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session3 = std::make_shared<MockSession>(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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(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<std::string>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message.data()), topic2); // rvalue
ctx.restart();
ctx.run();
MockSession* p1 = dynamic_cast<MockSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->message, topic1Message);
MockSession* p2 = dynamic_cast<MockSession*>(session2.get());
ASSERT_NE(p2, nullptr);
EXPECT_EQ(p2->message, topic2Message);
}
TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
{
std::shared_ptr<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(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<std::string>(topic1Message);
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message), topic2); // rvalue
ctx.restart();
ctx.run();
MockDeadSession* p1 = dynamic_cast<MockDeadSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->dead(), true);
MockSession* p2 = dynamic_cast<MockSession*>(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<std::string> subMap{ctx, "test"};
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
};
TEST_F(SubscriptionMapMockPrometheusTest, subscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, add(1));
subMap.subscribe(session, "topic");
ctx.run();
}
TEST_F(SubscriptionMapMockPrometheusTest, unsubscribe)
{
auto& counter = makeMock<GaugeInt>("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<MockDeadSession>(tagDecoratorFactory);
auto& counter = makeMock<GaugeInt>("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<std::string>("message"), "topic");
subMap.publish(
std::make_shared<std::string>("message"), "topic"
); // Dead session is detected only after failed send
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMapMockPrometheusTest, count)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, value());
subMap.count();
}

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <ripple/protocol/STObject.h>
#include <memory>
#include <vector>
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<BookChangesFeed>;
TEST_F(FeedBookChangeTest, Pub)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32);
auto transactions = std::vector<TransactionAndMetadata>{};
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());
}

View File

@@ -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 <gtest/gtest.h>
#include <memory>
#include <string>
// Base class for feed tests, providing easy way to access the received feed
template <typename TestedFeed>
class FeedBaseTest : public SyncAsioContextTest, public MockBackendTest {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<TestedFeed> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
MockBackendTest::SetUp();
testFeedPtr = std::make_shared<TestedFeed>(ctx);
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
MockBackendTest::TearDown();
SyncAsioContextTest::TearDown();
}
std::string const&
receivedFeedMessage() const
{
auto const mockSession = dynamic_cast<MockSession*>(sessionPtr.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
return mockSession->message;
}
void
cleanReceivedFeed()
{
auto mockSession = dynamic_cast<MockSession*>(sessionPtr.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
mockSession->message.clear();
}
};

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <memory>
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<NamedForwardFeedTest>;
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);
}

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/Fees.h>
#include <memory>
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
using namespace feed::impl;
namespace json = boost::json;
using FeedLedgerTest = FeedBaseTest<LedgerFeed>;
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();
}

View File

@@ -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 <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
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<ProposedTransactionFeed>;
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<web::ConnectionBase> const sessionIdle = std::make_shared<MockSession>(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<MockSession*>(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<MockSession>(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<MockSession>(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<web::ConnectionBase> sessionPtr;
std::shared_ptr<ProposedTransactionFeed> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
SyncAsioContextTest::TearDown();
}
};
TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
{
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
auto& counterAccount = makeMock<GaugeInt>("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<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
auto& counterAccount = makeMock<GaugeInt>("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();
}

View File

@@ -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 <boost/asio/io_context.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
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<web::ConnectionBase> sessionPtr;
std::shared_ptr<SingleFeedBase> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
SyncAsioContextTest::TearDown();
}
};
TEST_F(FeedBaseMockPrometheusTest, subUnsub)
{
auto& counter = makeMock<GaugeInt>("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<GaugeInt>("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<NamedSingleFeedTest>;
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);
}

View File

@@ -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 <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/Issue.h>
#include <ripple/protocol/STObject.h>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include <vector>
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<SubscriptionManager> SubscriptionManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<web::ConnectionBase> session;
void
SetUp() override
{
MockBackendTest::SetUp();
SyncAsioContextTest::SetUp();
SubscriptionManagerPtr = std::make_shared<SubscriptionManager>(ctx, backend);
session = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
session.reset();
SubscriptionManagerPtr.reset();
SyncAsioContextTest::TearDown();
MockBackendTest::TearDown();
}
std::string const&
receivedFeedMessage() const
{
auto const mockSession = dynamic_cast<MockSession*>(session.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
return mockSession->message;
}
void
cleanReceivedFeed()
{
auto mockSession = dynamic_cast<MockSession*>(session.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
mockSession->message.clear();
}
};
TEST_F(SubscriptionManagerTest, MultipleThreadCtx)
{
std::optional<boost::asio::io_context::work> work_;
work_.emplace(ctx); // guard the context
std::vector<std::thread> 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<boost::asio::io_context::executor_type> work_ = boost::asio::make_work_guard(ctx);
std::vector<std::thread> 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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> session2 = std::make_shared<MockSession>(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<TransactionAndMetadata>{};
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);
}

View File

@@ -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 <gtest/gtest.h>
#include <memory>
#include <string>
using namespace testing;
struct FeedTrackableSignalTests : Test {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
void
SetUp() override
{
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
}
};
TEST_F(FeedTrackableSignalTests, Connect)
{
feed::impl::TrackableSignal<web::ConnectionBase, std::string> 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<web::ConnectionBase, std::string> 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<std::string, web::ConnectionBase, std::string> 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<std::string, web::ConnectionBase, std::string> 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());
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<feed::SubscriptionManager>(ctx, backend);
util::TagDecoratorFactory const tagDecoratorFactory{util::Config{}};
session_ = std::make_shared<MockSession>(tagDecoratorFactory);
}
void

View File

@@ -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<MockETLService>();
rpcEngine = std::make_shared<MockAsyncRPCEngine>();
tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
subManager = std::make_shared<SubscriptionManager>(cfg, backend);
session = std::make_shared<MockWsBase>(*tagFactory);
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(
cfg, backend, rpcEngine, etl, subManager
);
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
}
void
@@ -91,7 +87,6 @@ protected:
std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
std::shared_ptr<MockETLService> etl;
std::shared_ptr<SubscriptionManager> subManager;
std::shared_ptr<util::TagDecoratorFactory> tagFactory;
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler;
std::shared_ptr<MockWsBase> session;
@@ -703,9 +698,8 @@ TEST_F(WebRPCServerHandlerTest, WsTooBusy)
session->upgraded = true;
auto localRpcEngine = std::make_shared<MockRPCEngine>();
auto localHandler = std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(
cfg, backend, localRpcEngine, etl, subManager
);
auto localHandler =
std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(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<MockRPCEngine>();
auto localHandler = std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(
cfg, backend, localRpcEngine, etl, subManager
);
auto localHandler =
std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(cfg, backend, localRpcEngine, etl);
static auto constexpr request = R"({
"method": "server_info",
"params": [{}]