Cover LoadBalancer with tests (#1394)

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

View File

@@ -39,7 +39,9 @@ target_sources(
handlers/NFTSellOffers.cpp
handlers/NoRippleCheck.cpp
handlers/Random.cpp
handlers/Subscribe.cpp
handlers/TransactionEntry.cpp
handlers/Unsubscribe.cpp
)
target_link_libraries(clio_rpc PRIVATE clio_util)

View File

@@ -21,6 +21,7 @@
#include "data/BackendInterface.hpp"
#include "data/DBHelpers.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "main/Build.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
@@ -51,9 +52,6 @@ namespace etl {
class ETLService;
class LoadBalancer;
} // namespace etl
namespace feed {
class SubscriptionManager;
} // namespace feed
namespace rpc {
class Counters;
} // namespace rpc
@@ -63,17 +61,16 @@ namespace rpc {
/**
* @brief Contains common functionality for handling the `server_info` command
*
* @tparam SubscriptionManagerType The type of the subscription manager
* @tparam LoadBalancerType The type of the load balancer
* @tparam ETLServiceType The type of the ETL service
* @tparam CountersType The type of the counters
*/
template <typename SubscriptionManagerType, typename LoadBalancerType, typename ETLServiceType, typename CountersType>
template <typename LoadBalancerType, typename ETLServiceType, typename CountersType>
class BaseServerInfoHandler {
static constexpr auto BACKEND_COUNTERS_KEY = "backend_counters";
std::shared_ptr<BackendInterface> backend_;
std::shared_ptr<SubscriptionManagerType> subscriptions_;
std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
std::shared_ptr<LoadBalancerType> balancer_;
std::shared_ptr<ETLServiceType const> etl_;
std::reference_wrapper<CountersType const> counters_;
@@ -159,7 +156,7 @@ public:
*/
BaseServerInfoHandler(
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<SubscriptionManagerType> const& subscriptions,
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions,
std::shared_ptr<LoadBalancerType> const& balancer,
std::shared_ptr<ETLServiceType const> const& etl,
CountersType const& counters
@@ -352,7 +349,6 @@ private:
*
* For more details see: https://xrpl.org/server_info-clio.html
*/
using ServerInfoHandler =
BaseServerInfoHandler<feed::SubscriptionManager, etl::LoadBalancer, etl::ETLService, Counters>;
using ServerInfoHandler = BaseServerInfoHandler<etl::LoadBalancer, etl::ETLService, Counters>;
} // namespace rpc

View File

@@ -0,0 +1,303 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "rpc/handlers/Subscribe.hpp"
#include "data/BackendInterface.hpp"
#include "data/Types.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Checkers.hpp"
#include "rpc/common/MetaProcessors.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <ripple/beast/utility/Zero.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace rpc {
SubscribeHandler::SubscribeHandler(
std::shared_ptr<BackendInterface> const& sharedPtrBackend,
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
)
: sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
{
}
RpcSpecConstRef
SubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_array())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
for (auto const& book : value.as_array()) {
if (!book.is_object())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}};
if (book.as_object().contains("taker")) {
if (auto err = meta::WithCustomError(
validation::AccountValidator,
Status{RippledError::rpcBAD_ISSUER, "Issuer account malformed."}
)
.verify(book.as_object(), "taker");
!err)
return err;
}
auto const parsedBook = parseBook(book.as_object());
if (auto const status = std::get_if<Status>(&parsedBook))
return Error(*status);
}
return MaybeError{};
}};
static auto const rpcSpec = RpcSpec{
{JS(streams), validation::SubscribeStreamValidator},
{JS(accounts), validation::SubscribeAccountsValidator},
{JS(accounts_proposed), validation::SubscribeAccountsValidator},
{JS(books), booksValidator},
{"user", check::Deprecated{}},
{JS(password), check::Deprecated{}},
{JS(rt_accounts), check::Deprecated{}}
};
return rpcSpec;
}
SubscribeHandler::Result
SubscribeHandler::process(Input input, Context const& ctx) const
{
auto output = Output{};
// Mimic rippled. No matter what the request is, the api version changes for the whole session
ctx.session->apiSubVersion = ctx.apiVersion;
if (input.streams) {
auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
if (!ledger.empty())
output.ledger = ledger;
}
if (input.accounts)
subscribeToAccounts(*(input.accounts), ctx.session);
if (input.accountsProposed)
subscribeToAccountsProposed(*(input.accountsProposed), ctx.session);
if (input.books)
subscribeToBooks(*(input.books), ctx.session, ctx.yield, output);
return output;
}
boost::json::object
SubscribeHandler::subscribeToStreams(
boost::asio::yield_context yield,
std::vector<std::string> const& streams,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
auto response = boost::json::object{};
for (auto const& stream : streams) {
if (stream == "ledger") {
response = subscriptions_->subLedger(yield, session);
} else if (stream == "transactions") {
subscriptions_->subTransactions(session);
} else if (stream == "transactions_proposed") {
subscriptions_->subProposedTransactions(session);
} else if (stream == "validations") {
subscriptions_->subValidation(session);
} else if (stream == "manifests") {
subscriptions_->subManifest(session);
} else if (stream == "book_changes") {
subscriptions_->subBookChanges(session);
}
}
return response;
}
void
SubscribeHandler::subscribeToAccountsProposed(
std::vector<std::string> const& accounts,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->subProposedAccount(*accountID, session);
}
}
void
SubscribeHandler::subscribeToAccounts(
std::vector<std::string> const& accounts,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->subAccount(*accountID, session);
}
}
void
SubscribeHandler::subscribeToBooks(
std::vector<OrderBook> const& books,
std::shared_ptr<web::ConnectionBase> const& session,
boost::asio::yield_context yield,
Output& output
) const
{
static auto constexpr fetchLimit = 200;
std::optional<data::LedgerRange> rng;
for (auto const& internalBook : books) {
if (internalBook.snapshot) {
if (!rng)
rng = sharedPtrBackend_->fetchLedgerRange();
auto const getOrderBook = [&](auto const& book, auto& snapshots) {
auto const bookBase = getBookBase(book);
auto const [offers, _] =
sharedPtrBackend_->fetchBookOffers(bookBase, rng->maxSequence, fetchLimit, yield);
// the taker is not really uesed, same issue with
// https://github.com/XRPLF/xrpl-dev-portal/issues/1818
auto const takerID = internalBook.taker ? accountFromStringStrict(*(internalBook.taker)) : beast::zero;
auto const orderBook =
postProcessOrderBook(offers, book, *takerID, *sharedPtrBackend_, rng->maxSequence, yield);
std::copy(orderBook.begin(), orderBook.end(), std::back_inserter(snapshots));
};
if (internalBook.both) {
if (!output.bids)
output.bids = boost::json::array();
if (!output.asks)
output.asks = boost::json::array();
getOrderBook(internalBook.book, *(output.bids));
getOrderBook(ripple::reversed(internalBook.book), *(output.asks));
} else {
if (!output.offers)
output.offers = boost::json::array();
getOrderBook(internalBook.book, *(output.offers));
}
}
subscriptions_->subBook(internalBook.book, session);
if (internalBook.both)
subscriptions_->subBook(ripple::reversed(internalBook.book), session);
}
}
void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, SubscribeHandler::Output const& output)
{
jv = output.ledger ? *(output.ledger) : boost::json::object();
if (output.offers)
jv.as_object().emplace(JS(offers), *(output.offers));
if (output.asks)
jv.as_object().emplace(JS(asks), *(output.asks));
if (output.bids)
jv.as_object().emplace(JS(bids), *(output.bids));
}
SubscribeHandler::Input
tag_invoke(boost::json::value_to_tag<SubscribeHandler::Input>, boost::json::value const& jv)
{
auto input = SubscribeHandler::Input{};
auto const& jsonObject = jv.as_object();
if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
input.streams = std::vector<std::string>();
for (auto const& stream : streams->value().as_array())
input.streams->push_back(boost::json::value_to<std::string>(stream));
}
if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
input.accounts = std::vector<std::string>();
for (auto const& account : accounts->value().as_array())
input.accounts->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end()) {
input.accountsProposed = std::vector<std::string>();
for (auto const& account : accountsProposed->value().as_array())
input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
input.books = std::vector<SubscribeHandler::OrderBook>();
for (auto const& book : books->value().as_array()) {
auto internalBook = SubscribeHandler::OrderBook{};
auto const& bookObject = book.as_object();
if (auto const& taker = bookObject.find(JS(taker)); taker != bookObject.end())
internalBook.taker = boost::json::value_to<std::string>(taker->value());
if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
internalBook.both = both->value().as_bool();
if (auto const& snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end())
internalBook.snapshot = snapshot->value().as_bool();
auto const parsedBookMaybe = parseBook(book.as_object());
internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
input.books->push_back(internalBook);
}
}
return input;
}
} // namespace rpc

View File

@@ -20,14 +20,9 @@
#pragma once
#include "data/BackendInterface.hpp"
#include "data/Types.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Checkers.hpp"
#include "rpc/common/MetaProcessors.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
@@ -44,25 +39,20 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace feed {
class SubscriptionManager;
} // namespace feed
namespace rpc {
/**
* @brief Contains functionality for handling the `subscribe` command
* @brief Contains functionality for handling the `subscribe` command.
* The subscribe method requests periodic notifications from the server when certain events happen.
*
* @tparam SubscriptionManagerType The type of the subscription manager to use
* For more details see: https://xrpl.org/subscribe.html
*/
template <typename SubscriptionManagerType>
class BaseSubscribeHandler {
class SubscribeHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
std::shared_ptr<SubscriptionManagerType> subscriptions_;
std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
public:
/**
@@ -109,13 +99,10 @@ public:
* @param sharedPtrBackend The backend to use
* @param subscriptions The subscription manager to use
*/
BaseSubscribeHandler(
SubscribeHandler(
std::shared_ptr<BackendInterface> const& sharedPtrBackend,
std::shared_ptr<SubscriptionManagerType> const& subscriptions
)
: sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
{
}
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
);
/**
* @brief Returns the API specification for the command
@@ -124,53 +111,7 @@ public:
* @return The spec for the given apiVersion
*/
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_array())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
for (auto const& book : value.as_array()) {
if (!book.is_object())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}};
if (book.as_object().contains("taker")) {
if (auto err = meta::WithCustomError(
validation::AccountValidator,
Status{RippledError::rpcBAD_ISSUER, "Issuer account malformed."}
)
.verify(book.as_object(), "taker");
!err)
return err;
}
auto const parsedBook = parseBook(book.as_object());
if (auto const status = std::get_if<Status>(&parsedBook))
return Error(*status);
}
return MaybeError{};
}};
static auto const rpcSpec = RpcSpec{
{JS(streams), validation::SubscribeStreamValidator},
{JS(accounts), validation::SubscribeAccountsValidator},
{JS(accounts_proposed), validation::SubscribeAccountsValidator},
{JS(books), booksValidator},
{"user", check::Deprecated{}},
{JS(password), check::Deprecated{}},
{JS(rt_accounts), check::Deprecated{}}
};
return rpcSpec;
}
spec([[maybe_unused]] uint32_t apiVersion);
/**
* @brief Process the Subscribe command
@@ -180,30 +121,7 @@ public:
* @return The result of the operation
*/
Result
process(Input input, Context const& ctx) const
{
auto output = Output{};
// Mimic rippled. No matter what the request is, the api version changes for the whole session
ctx.session->apiSubVersion = ctx.apiVersion;
if (input.streams) {
auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
if (!ledger.empty())
output.ledger = ledger;
}
if (input.accounts)
subscribeToAccounts(*(input.accounts), ctx.session);
if (input.accountsProposed)
subscribeToAccountsProposed(*(input.accountsProposed), ctx.session);
if (input.books)
subscribeToBooks(*(input.books), ctx.session, ctx.yield, output);
return output;
}
process(Input input, Context const& ctx) const;
private:
boost::json::object
@@ -211,50 +129,17 @@ private:
boost::asio::yield_context yield,
std::vector<std::string> const& streams,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
auto response = boost::json::object{};
for (auto const& stream : streams) {
if (stream == "ledger") {
response = subscriptions_->subLedger(yield, session);
} else if (stream == "transactions") {
subscriptions_->subTransactions(session);
} else if (stream == "transactions_proposed") {
subscriptions_->subProposedTransactions(session);
} else if (stream == "validations") {
subscriptions_->subValidation(session);
} else if (stream == "manifests") {
subscriptions_->subManifest(session);
} else if (stream == "book_changes") {
subscriptions_->subBookChanges(session);
}
}
return response;
}
) const;
void
subscribeToAccounts(std::vector<std::string> const& accounts, std::shared_ptr<web::ConnectionBase> const& session)
const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->subAccount(*accountID, session);
}
}
const;
void
subscribeToAccountsProposed(
std::vector<std::string> const& accounts,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->subProposedAccount(*accountID, session);
}
}
) const;
void
subscribeToBooks(
@@ -262,121 +147,25 @@ private:
std::shared_ptr<web::ConnectionBase> const& session,
boost::asio::yield_context yield,
Output& output
) const
{
static auto constexpr fetchLimit = 200;
std::optional<data::LedgerRange> rng;
for (auto const& internalBook : books) {
if (internalBook.snapshot) {
if (!rng)
rng = sharedPtrBackend_->fetchLedgerRange();
auto const getOrderBook = [&](auto const& book, auto& snapshots) {
auto const bookBase = getBookBase(book);
auto const [offers, _] =
sharedPtrBackend_->fetchBookOffers(bookBase, rng->maxSequence, fetchLimit, yield);
// the taker is not really uesed, same issue with
// https://github.com/XRPLF/xrpl-dev-portal/issues/1818
auto const takerID =
internalBook.taker ? accountFromStringStrict(*(internalBook.taker)) : beast::zero;
auto const orderBook =
postProcessOrderBook(offers, book, *takerID, *sharedPtrBackend_, rng->maxSequence, yield);
std::copy(orderBook.begin(), orderBook.end(), std::back_inserter(snapshots));
};
if (internalBook.both) {
if (!output.bids)
output.bids = boost::json::array();
if (!output.asks)
output.asks = boost::json::array();
getOrderBook(internalBook.book, *(output.bids));
getOrderBook(ripple::reversed(internalBook.book), *(output.asks));
} else {
if (!output.offers)
output.offers = boost::json::array();
getOrderBook(internalBook.book, *(output.offers));
}
}
subscriptions_->subBook(internalBook.book, session);
if (internalBook.both)
subscriptions_->subBook(ripple::reversed(internalBook.book), session);
}
}
) const;
/**
* @brief Convert output to json value
*
* @param jv The json value to convert to
* @param output The output to convert from
*/
friend void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output)
{
jv = output.ledger ? *(output.ledger) : boost::json::object();
if (output.offers)
jv.as_object().emplace(JS(offers), *(output.offers));
if (output.asks)
jv.as_object().emplace(JS(asks), *(output.asks));
if (output.bids)
jv.as_object().emplace(JS(bids), *(output.bids));
}
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
/**
* @brief Convert json value to input
*
* @param jv The json value to convert from
* @return The input to convert to
*/
friend Input
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv)
{
auto input = Input{};
auto const& jsonObject = jv.as_object();
if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
input.streams = std::vector<std::string>();
for (auto const& stream : streams->value().as_array())
input.streams->push_back(boost::json::value_to<std::string>(stream));
}
if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
input.accounts = std::vector<std::string>();
for (auto const& account : accounts->value().as_array())
input.accounts->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed));
accountsProposed != jsonObject.end()) {
input.accountsProposed = std::vector<std::string>();
for (auto const& account : accountsProposed->value().as_array())
input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
input.books = std::vector<OrderBook>();
for (auto const& book : books->value().as_array()) {
auto internalBook = OrderBook{};
auto const& bookObject = book.as_object();
if (auto const& taker = bookObject.find(JS(taker)); taker != bookObject.end())
internalBook.taker = boost::json::value_to<std::string>(taker->value());
if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
internalBook.both = both->value().as_bool();
if (auto const& snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end())
internalBook.snapshot = snapshot->value().as_bool();
auto const parsedBookMaybe = parseBook(book.as_object());
internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
input.books->push_back(internalBook);
}
}
return input;
}
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
};
/**
* @brief The subscribe method requests periodic notifications from the server when certain events happen.
*
* For more details see: https://xrpl.org/subscribe.html
*/
using SubscribeHandler = BaseSubscribeHandler<feed::SubscriptionManager>;
} // namespace rpc

View File

@@ -0,0 +1,208 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "rpc/handlers/Unsubscribe.hpp"
#include "data/BackendInterface.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Checkers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include "util/Assert.hpp"
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace rpc {
UnsubscribeHandler::UnsubscribeHandler(
std::shared_ptr<BackendInterface> const& sharedPtrBackend,
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
)
: sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
{
}
RpcSpecConstRef
UnsubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_array())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
for (auto const& book : value.as_array()) {
if (!book.is_object())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
auto const parsedBook = parseBook(book.as_object());
if (auto const status = std::get_if<Status>(&parsedBook))
return Error(*status);
}
return MaybeError{};
}};
static auto const rpcSpec = RpcSpec{
{JS(streams), validation::SubscribeStreamValidator},
{JS(accounts), validation::SubscribeAccountsValidator},
{JS(accounts_proposed), validation::SubscribeAccountsValidator},
{JS(books), booksValidator},
{JS(url), check::Deprecated{}},
{JS(rt_accounts), check::Deprecated{}},
{"rt_transactions", check::Deprecated{}},
};
return rpcSpec;
}
UnsubscribeHandler::Result
UnsubscribeHandler::process(Input input, Context const& ctx) const
{
if (input.streams)
unsubscribeFromStreams(*(input.streams), ctx.session);
if (input.accounts)
unsubscribeFromAccounts(*(input.accounts), ctx.session);
if (input.accountsProposed)
unsubscribeFromProposedAccounts(*(input.accountsProposed), ctx.session);
if (input.books)
unsubscribeFromBooks(*(input.books), ctx.session);
return Output{};
}
void
UnsubscribeHandler::unsubscribeFromStreams(
std::vector<std::string> const& streams,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& stream : streams) {
if (stream == "ledger") {
subscriptions_->unsubLedger(session);
} else if (stream == "transactions") {
subscriptions_->unsubTransactions(session);
} else if (stream == "transactions_proposed") {
subscriptions_->unsubProposedTransactions(session);
} else if (stream == "validations") {
subscriptions_->unsubValidation(session);
} else if (stream == "manifests") {
subscriptions_->unsubManifest(session);
} else if (stream == "book_changes") {
subscriptions_->unsubBookChanges(session);
} else {
ASSERT(false, "Unknown stream: {}", stream);
}
}
}
void
UnsubscribeHandler::unsubscribeFromAccounts(
std::vector<std::string> accounts,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->unsubAccount(*accountID, session);
}
}
void
UnsubscribeHandler::unsubscribeFromProposedAccounts(
std::vector<std::string> accountsProposed,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accountsProposed) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->unsubProposedAccount(*accountID, session);
}
}
void
UnsubscribeHandler::unsubscribeFromBooks(
std::vector<OrderBook> const& books,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& orderBook : books) {
subscriptions_->unsubBook(orderBook.book, session);
if (orderBook.both)
subscriptions_->unsubBook(ripple::reversed(orderBook.book), session);
}
}
UnsubscribeHandler::Input
tag_invoke(boost::json::value_to_tag<UnsubscribeHandler::Input>, boost::json::value const& jv)
{
auto input = UnsubscribeHandler::Input{};
auto const& jsonObject = jv.as_object();
if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
input.streams = std::vector<std::string>();
for (auto const& stream : streams->value().as_array())
input.streams->push_back(boost::json::value_to<std::string>(stream));
}
if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
input.accounts = std::vector<std::string>();
for (auto const& account : accounts->value().as_array())
input.accounts->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end()) {
input.accountsProposed = std::vector<std::string>();
for (auto const& account : accountsProposed->value().as_array())
input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
input.books = std::vector<UnsubscribeHandler::OrderBook>();
for (auto const& book : books->value().as_array()) {
auto internalBook = UnsubscribeHandler::OrderBook{};
auto const& bookObject = book.as_object();
if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
internalBook.both = both->value().as_bool();
auto const parsedBookMaybe = parseBook(book.as_object());
internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
input.books->push_back(internalBook);
}
}
return input;
}
} // namespace rpc

View File

@@ -20,17 +20,13 @@
#pragma once
#include "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Checkers.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include "util/Assert.hpp"
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
@@ -39,8 +35,6 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace feed {
@@ -50,12 +44,16 @@ class SubscriptionManager;
namespace rpc {
/**
* @brief Handles the `unsubscribe` command which is used to disconnect a subscriber from a feed
* @brief Handles the `unsubscribe` command which is used to disconnect a subscriber from a feed.
* The unsubscribe command tells the server to stop sending messages for a particular subscription or set of
* subscriptions.
*
* For more details see: https://xrpl.org/unsubscribe.html
*/
template <typename SubscriptionManagerType>
class BaseUnsubscribeHandler {
class UnsubscribeHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
std::shared_ptr<SubscriptionManagerType> subscriptions_;
std::shared_ptr<feed::SubscriptionManagerInterface> subscriptions_;
public:
/**
@@ -85,13 +83,10 @@ public:
* @param sharedPtrBackend The backend to use
* @param subscriptions The subscription manager to use
*/
BaseUnsubscribeHandler(
UnsubscribeHandler(
std::shared_ptr<BackendInterface> const& sharedPtrBackend,
std::shared_ptr<SubscriptionManagerType> const& subscriptions
)
: sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
{
}
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptions
);
/**
* @brief Returns the API specification for the command
@@ -100,40 +95,7 @@ public:
* @return The spec for the given apiVersion
*/
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_array())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
for (auto const& book : value.as_array()) {
if (!book.is_object())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
return Error{Status{RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
auto const parsedBook = parseBook(book.as_object());
if (auto const status = std::get_if<Status>(&parsedBook))
return Error(*status);
}
return MaybeError{};
}};
static auto const rpcSpec = RpcSpec{
{JS(streams), validation::SubscribeStreamValidator},
{JS(accounts), validation::SubscribeAccountsValidator},
{JS(accounts_proposed), validation::SubscribeAccountsValidator},
{JS(books), booksValidator},
{JS(url), check::Deprecated{}},
{JS(rt_accounts), check::Deprecated{}},
{"rt_transactions", check::Deprecated{}},
};
return rpcSpec;
}
spec([[maybe_unused]] uint32_t apiVersion);
/**
* @brief Process the Unsubscribe command
@@ -143,127 +105,35 @@ public:
* @return The result of the operation
*/
Result
process(Input input, Context const& ctx) const
{
if (input.streams)
unsubscribeFromStreams(*(input.streams), ctx.session);
if (input.accounts)
unsubscribeFromAccounts(*(input.accounts), ctx.session);
if (input.accountsProposed)
unsubscribeFromProposedAccounts(*(input.accountsProposed), ctx.session);
if (input.books)
unsubscribeFromBooks(*(input.books), ctx.session);
return Output{};
}
process(Input input, Context const& ctx) const;
private:
void
unsubscribeFromStreams(std::vector<std::string> const& streams, std::shared_ptr<web::ConnectionBase> const& session)
const
{
for (auto const& stream : streams) {
if (stream == "ledger") {
subscriptions_->unsubLedger(session);
} else if (stream == "transactions") {
subscriptions_->unsubTransactions(session);
} else if (stream == "transactions_proposed") {
subscriptions_->unsubProposedTransactions(session);
} else if (stream == "validations") {
subscriptions_->unsubValidation(session);
} else if (stream == "manifests") {
subscriptions_->unsubManifest(session);
} else if (stream == "book_changes") {
subscriptions_->unsubBookChanges(session);
} else {
ASSERT(false, "Unknown stream: {}", stream);
}
}
}
const;
void
unsubscribeFromAccounts(std::vector<std::string> accounts, std::shared_ptr<web::ConnectionBase> const& session)
const
{
for (auto const& account : accounts) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->unsubAccount(*accountID, session);
}
}
const;
void
unsubscribeFromProposedAccounts(
std::vector<std::string> accountsProposed,
std::shared_ptr<web::ConnectionBase> const& session
) const
{
for (auto const& account : accountsProposed) {
auto const accountID = accountFromStringStrict(account);
subscriptions_->unsubProposedAccount(*accountID, session);
}
}
) const;
void
unsubscribeFromBooks(std::vector<OrderBook> const& books, std::shared_ptr<web::ConnectionBase> const& session) const
{
for (auto const& orderBook : books) {
subscriptions_->unsubBook(orderBook.book, session);
if (orderBook.both)
subscriptions_->unsubBook(ripple::reversed(orderBook.book), session);
}
}
unsubscribeFromBooks(std::vector<OrderBook> const& books, std::shared_ptr<web::ConnectionBase> const& session)
const;
/**
* @brief Convert a JSON object to an Input
*
* @param jv The JSON object to convert
* @return The Input object
*/
friend Input
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv)
{
auto input = Input{};
auto const& jsonObject = jv.as_object();
if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) {
input.streams = std::vector<std::string>();
for (auto const& stream : streams->value().as_array())
input.streams->push_back(boost::json::value_to<std::string>(stream));
}
if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) {
input.accounts = std::vector<std::string>();
for (auto const& account : accounts->value().as_array())
input.accounts->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed));
accountsProposed != jsonObject.end()) {
input.accountsProposed = std::vector<std::string>();
for (auto const& account : accountsProposed->value().as_array())
input.accountsProposed->push_back(boost::json::value_to<std::string>(account));
}
if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) {
input.books = std::vector<OrderBook>();
for (auto const& book : books->value().as_array()) {
auto internalBook = OrderBook{};
auto const& bookObject = book.as_object();
if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
internalBook.both = both->value().as_bool();
auto const parsedBookMaybe = parseBook(book.as_object());
internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
input.books->push_back(internalBook);
}
}
return input;
}
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
};
/**
* @brief The unsubscribe command tells the server to stop sending messages for a particular subscription or set of
* subscriptions.
*
* For more details see: https://xrpl.org/unsubscribe.html
*/
using UnsubscribeHandler = BaseUnsubscribeHandler<feed::SubscriptionManager>;
} // namespace rpc