mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -1,4 +1,4 @@
|
||||
add_library(clio_app)
|
||||
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp WebHandlers.cpp)
|
||||
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp Stopper.cpp WebHandlers.cpp)
|
||||
|
||||
target_link_libraries(clio_app PUBLIC clio_etl clio_etlng clio_feed clio_web clio_rpc clio_migration)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "app/ClioApplication.hpp"
|
||||
|
||||
#include "app/Stopper.hpp"
|
||||
#include "app/WebHandlers.hpp"
|
||||
#include "data/AmendmentCenter.hpp"
|
||||
#include "data/BackendFactory.hpp"
|
||||
@@ -45,6 +46,8 @@
|
||||
#include "web/ng/Server.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/use_future.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -84,6 +87,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
|
||||
{
|
||||
LOG(util::LogService::info()) << "Clio version: " << util::build::getClioFullVersionString();
|
||||
PrometheusService::init(config);
|
||||
signalsHandler_.subscribeToStop([this]() { appStopper_.stop(); });
|
||||
}
|
||||
|
||||
int
|
||||
@@ -169,6 +173,10 @@ ClioApplication::run(bool const useNgWebServer)
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
appStopper_.setOnStop(
|
||||
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
|
||||
);
|
||||
|
||||
// Blocks until stopped.
|
||||
// When stopped, shared_ptrs fall out of scope
|
||||
// Calls destructors on all resources, and destructs in order
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "app/Stopper.hpp"
|
||||
#include "util/SignalsHandler.hpp"
|
||||
#include "util/newconfig/ConfigDefinition.hpp"
|
||||
|
||||
@@ -30,6 +31,7 @@ namespace app {
|
||||
class ClioApplication {
|
||||
util::config::ClioConfigDefinition const& config_;
|
||||
util::SignalsHandler signalsHandler_;
|
||||
Stopper appStopper_;
|
||||
|
||||
public:
|
||||
/**
|
||||
|
||||
52
src/app/Stopper.cpp
Normal file
52
src/app/Stopper.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, 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 "app/Stopper.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace app {
|
||||
|
||||
Stopper::~Stopper()
|
||||
{
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
|
||||
void
|
||||
Stopper::setOnStop(std::function<void(boost::asio::yield_context)> cb)
|
||||
{
|
||||
boost::asio::spawn(ctx_, std::move(cb));
|
||||
}
|
||||
|
||||
void
|
||||
Stopper::stop()
|
||||
{
|
||||
// Do nothing if worker_ is already running
|
||||
if (worker_.joinable())
|
||||
return;
|
||||
|
||||
worker_ = std::thread{[this]() { ctx_.run(); }};
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
118
src/app/Stopper.hpp
Normal file
118
src/app/Stopper.hpp
Normal file
@@ -0,0 +1,118 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "etl/ETLService.hpp"
|
||||
#include "etl/LoadBalancer.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
namespace app {
|
||||
|
||||
/**
|
||||
* @brief Application stopper class. On stop it will create a new thread to run all the shutdown tasks.
|
||||
*/
|
||||
class Stopper {
|
||||
boost::asio::io_context ctx_;
|
||||
std::thread worker_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Destroy the Stopper object
|
||||
*/
|
||||
~Stopper();
|
||||
|
||||
/**
|
||||
* @brief Set the callback to be called when the application is stopped.
|
||||
*
|
||||
* @param cb The callback to be called on application stop.
|
||||
*/
|
||||
void
|
||||
setOnStop(std::function<void(boost::asio::yield_context)> cb);
|
||||
|
||||
/**
|
||||
* @brief Stop the application and run the shutdown tasks.
|
||||
*/
|
||||
void
|
||||
stop();
|
||||
|
||||
/**
|
||||
* @brief Create a callback to be called on application stop.
|
||||
*
|
||||
* @param server The server to stop.
|
||||
* @param balancer The load balancer to stop.
|
||||
* @param etl The ETL service to stop.
|
||||
* @param subscriptions The subscription manager to stop.
|
||||
* @param backend The backend to stop.
|
||||
* @param ioc The io_context to stop.
|
||||
* @return The callback to be called on application stop.
|
||||
*/
|
||||
template <
|
||||
web::ng::SomeServer ServerType,
|
||||
etl::SomeLoadBalancer LoadBalancerType,
|
||||
etl::SomeETLService ETLServiceType>
|
||||
static std::function<void(boost::asio::yield_context)>
|
||||
makeOnStopCallback(
|
||||
ServerType& server,
|
||||
LoadBalancerType& balancer,
|
||||
ETLServiceType& etl,
|
||||
feed::SubscriptionManagerInterface& subscriptions,
|
||||
data::BackendInterface& backend,
|
||||
boost::asio::io_context& ioc
|
||||
)
|
||||
{
|
||||
return [&](boost::asio::yield_context yield) {
|
||||
util::CoroutineGroup coroutineGroup{yield};
|
||||
coroutineGroup.spawn(yield, [&server](auto innerYield) {
|
||||
server.stop(innerYield);
|
||||
LOG(util::LogService::info()) << "Server stopped";
|
||||
});
|
||||
coroutineGroup.spawn(yield, [&balancer](auto innerYield) {
|
||||
balancer.stop(innerYield);
|
||||
LOG(util::LogService::info()) << "LoadBalancer stopped";
|
||||
});
|
||||
coroutineGroup.asyncWait(yield);
|
||||
|
||||
etl.stop();
|
||||
LOG(util::LogService::info()) << "ETL stopped";
|
||||
|
||||
subscriptions.stop();
|
||||
LOG(util::LogService::info()) << "SubscriptionManager stopped";
|
||||
|
||||
backend.waitForWritesToFinish();
|
||||
LOG(util::LogService::info()) << "Backend writes finished";
|
||||
|
||||
ioc.stop();
|
||||
LOG(util::LogService::info()) << "io_context stopped";
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
@@ -683,6 +683,12 @@ public:
|
||||
bool
|
||||
finishWrites(std::uint32_t ledgerSequence);
|
||||
|
||||
/**
|
||||
* @brief Wait for all pending writes to finish.
|
||||
*/
|
||||
virtual void
|
||||
waitForWritesToFinish() = 0;
|
||||
|
||||
/**
|
||||
* @brief Mark the migration status of a migrator as Migrated in the database
|
||||
*
|
||||
|
||||
@@ -188,11 +188,16 @@ public:
|
||||
return {txns, {}};
|
||||
}
|
||||
|
||||
void
|
||||
waitForWritesToFinish() override
|
||||
{
|
||||
executor_.sync();
|
||||
}
|
||||
|
||||
bool
|
||||
doFinishWrites() override
|
||||
{
|
||||
// wait for other threads to finish their writes
|
||||
executor_.sync();
|
||||
waitForWritesToFinish();
|
||||
|
||||
if (!range_) {
|
||||
executor_.writeSync(schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_);
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -58,6 +59,16 @@ struct NFTsData;
|
||||
*/
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief A tag class to help identify ETLService in templated code.
|
||||
*/
|
||||
struct ETLServiceTag {
|
||||
virtual ~ETLServiceTag() = default;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept SomeETLService = std::derived_from<T, ETLServiceTag>;
|
||||
|
||||
/**
|
||||
* @brief This class is responsible for continuously extracting data from a p2p node, and writing that data to the
|
||||
* databases.
|
||||
@@ -71,7 +82,7 @@ namespace etl {
|
||||
* the others will fall back to monitoring/publishing. In this sense, this class dynamically transitions from monitoring
|
||||
* to writing and from writing to monitoring, based on the activity of other processes running on different machines.
|
||||
*/
|
||||
class ETLService {
|
||||
class ETLService : public ETLServiceTag {
|
||||
// TODO: make these template parameters in ETLService
|
||||
using LoadBalancerType = LoadBalancer;
|
||||
using DataPipeType = etl::impl::ExtractionDataPipe<org::xrpl::rpc::v1::GetLedgerResponse>;
|
||||
@@ -159,10 +170,20 @@ public:
|
||||
/**
|
||||
* @brief Stops components and joins worker thread.
|
||||
*/
|
||||
~ETLService()
|
||||
~ETLService() override
|
||||
{
|
||||
LOG(log_.info()) << "onStop called";
|
||||
LOG(log_.debug()) << "Stopping Reporting ETL";
|
||||
if (not state_.isStopping)
|
||||
stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop the ETL service.
|
||||
* @note This method blocks until the ETL service has stopped.
|
||||
*/
|
||||
void
|
||||
stop()
|
||||
{
|
||||
LOG(log_.info()) << "Stop called";
|
||||
|
||||
state_.isStopping = true;
|
||||
cacheLoader_.stop();
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Random.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
@@ -336,6 +337,16 @@ LoadBalancer::getETLState() noexcept
|
||||
return etlState_;
|
||||
}
|
||||
|
||||
void
|
||||
LoadBalancer::stop(boost::asio::yield_context yield)
|
||||
{
|
||||
util::CoroutineGroup group{yield};
|
||||
std::ranges::for_each(sources_, [&group, yield](auto& source) {
|
||||
group.spawn(yield, [&source](boost::asio::yield_context innerYield) { source->stop(innerYield); });
|
||||
});
|
||||
group.asyncWait(yield);
|
||||
}
|
||||
|
||||
void
|
||||
LoadBalancer::chooseForwardingSource()
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
@@ -51,6 +52,16 @@
|
||||
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief A tag class to help identify LoadBalancer in templated code.
|
||||
*/
|
||||
struct LoadBalancerTag {
|
||||
virtual ~LoadBalancerTag() = default;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept SomeLoadBalancer = std::derived_from<T, LoadBalancerTag>;
|
||||
|
||||
/**
|
||||
* @brief This class is used to manage connections to transaction processing processes.
|
||||
*
|
||||
@@ -58,7 +69,7 @@ namespace etl {
|
||||
* which ledgers have been validated by the network, and the range of ledgers each etl source has). This class also
|
||||
* allows requests for ledger data to be load balanced across all possible ETL sources.
|
||||
*/
|
||||
class LoadBalancer {
|
||||
class LoadBalancer : public LoadBalancerTag {
|
||||
public:
|
||||
using RawLedgerObjectType = org::xrpl::rpc::v1::RawLedgerObject;
|
||||
using GetLedgerResponseType = org::xrpl::rpc::v1::GetLedgerResponse;
|
||||
@@ -132,7 +143,7 @@ public:
|
||||
SourceFactory sourceFactory = makeSource
|
||||
);
|
||||
|
||||
~LoadBalancer();
|
||||
~LoadBalancer() override;
|
||||
|
||||
/**
|
||||
* @brief Load the initial ledger, writing data to the queue.
|
||||
@@ -203,6 +214,15 @@ public:
|
||||
std::optional<ETLState>
|
||||
getETLState() noexcept;
|
||||
|
||||
/**
|
||||
* @brief Stop the load balancer. This will stop all subscription sources.
|
||||
* @note This function will asynchronously wait for all sources to stop.
|
||||
*
|
||||
* @param yield The coroutine context
|
||||
*/
|
||||
void
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Execute a function on a randomly selected source.
|
||||
|
||||
@@ -65,6 +65,15 @@ public:
|
||||
virtual void
|
||||
run() = 0;
|
||||
|
||||
/**
|
||||
* @brief Stop Source.
|
||||
* @note This method will asynchronously wait for source to be stopped.
|
||||
*
|
||||
* @param yield The coroutine context.
|
||||
*/
|
||||
virtual void
|
||||
stop(boost::asio::yield_context yield) = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if source is connected
|
||||
*
|
||||
|
||||
@@ -102,6 +102,12 @@ public:
|
||||
subscriptionSource_->run();
|
||||
}
|
||||
|
||||
void
|
||||
stop(boost::asio::yield_context yield) final
|
||||
{
|
||||
subscriptionSource_->stop(yield);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if source is connected
|
||||
*
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -92,15 +91,6 @@ SubscriptionSource::SubscriptionSource(
|
||||
.setConnectionTimeout(wsTimeout_);
|
||||
}
|
||||
|
||||
SubscriptionSource::~SubscriptionSource()
|
||||
{
|
||||
stop();
|
||||
retry_.cancel();
|
||||
|
||||
if (runFuture_.valid())
|
||||
runFuture_.wait();
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionSource::run()
|
||||
{
|
||||
@@ -157,59 +147,53 @@ SubscriptionSource::validatedRange() const
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionSource::stop()
|
||||
SubscriptionSource::stop(boost::asio::yield_context yield)
|
||||
{
|
||||
stop_ = true;
|
||||
stopHelper_.asyncWaitForStop(yield);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionSource::subscribe()
|
||||
{
|
||||
runFuture_ = boost::asio::spawn(
|
||||
strand_,
|
||||
[this, _ = boost::asio::make_work_guard(strand_)](boost::asio::yield_context yield) {
|
||||
auto connection = wsConnectionBuilder_.connect(yield);
|
||||
if (not connection) {
|
||||
handleError(connection.error(), yield);
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::spawn(strand_, [this, _ = boost::asio::make_work_guard(strand_)](boost::asio::yield_context yield) {
|
||||
if (auto connection = wsConnectionBuilder_.connect(yield); connection) {
|
||||
wsConnection_ = std::move(connection).value();
|
||||
} else {
|
||||
handleError(connection.error(), yield);
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& subscribeCommand = getSubscribeCommandJson();
|
||||
auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield, wsTimeout_);
|
||||
if (writeErrorOpt) {
|
||||
handleError(writeErrorOpt.value(), yield);
|
||||
auto const& subscribeCommand = getSubscribeCommandJson();
|
||||
|
||||
if (auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield, wsTimeout_); writeErrorOpt) {
|
||||
handleError(writeErrorOpt.value(), yield);
|
||||
return;
|
||||
}
|
||||
|
||||
isConnected_ = true;
|
||||
LOG(log_.info()) << "Connected";
|
||||
onConnect_();
|
||||
|
||||
retry_.reset();
|
||||
|
||||
while (!stop_) {
|
||||
auto const message = wsConnection_->read(yield, wsTimeout_);
|
||||
if (not message) {
|
||||
handleError(message.error(), yield);
|
||||
return;
|
||||
}
|
||||
|
||||
isConnected_ = true;
|
||||
LOG(log_.info()) << "Connected";
|
||||
onConnect_();
|
||||
|
||||
retry_.reset();
|
||||
|
||||
while (!stop_) {
|
||||
auto const message = wsConnection_->read(yield, wsTimeout_);
|
||||
if (not message) {
|
||||
handleError(message.error(), yield);
|
||||
return;
|
||||
}
|
||||
|
||||
auto const handleErrorOpt = handleMessage(message.value());
|
||||
if (handleErrorOpt) {
|
||||
handleError(handleErrorOpt.value(), yield);
|
||||
return;
|
||||
}
|
||||
if (auto const handleErrorOpt = handleMessage(message.value()); handleErrorOpt) {
|
||||
handleError(handleErrorOpt.value(), yield);
|
||||
return;
|
||||
}
|
||||
// Close the connection
|
||||
handleError(
|
||||
util::requests::RequestError{"Subscription source stopped", boost::asio::error::operation_aborted},
|
||||
yield
|
||||
);
|
||||
},
|
||||
boost::asio::use_future
|
||||
);
|
||||
}
|
||||
// Close the connection
|
||||
handleError(
|
||||
util::requests::RequestError{"Subscription source stopped", boost::asio::error::operation_aborted}, yield
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<util::requests::RequestError>
|
||||
@@ -299,6 +283,8 @@ SubscriptionSource::handleError(util::requests::RequestError const& error, boost
|
||||
logError(error);
|
||||
if (not stop_) {
|
||||
retry_.retry([this] { subscribe(); });
|
||||
} else {
|
||||
stopHelper_.readyToStop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "util/Mutex.hpp"
|
||||
#include "util/Retry.hpp"
|
||||
#include "util/StopHelper.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "util/requests/Types.hpp"
|
||||
@@ -39,7 +40,6 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -50,6 +50,7 @@ namespace etl::impl {
|
||||
|
||||
/**
|
||||
* @brief This class is used to subscribe to a source of ledger data and forward it to the subscription manager.
|
||||
* @note This class is safe to delete only if io_context is stopped.
|
||||
*/
|
||||
class SubscriptionSource {
|
||||
public:
|
||||
@@ -89,7 +90,7 @@ private:
|
||||
|
||||
std::reference_wrapper<util::prometheus::GaugeInt> lastMessageTimeSecondsSinceEpoch_;
|
||||
|
||||
std::future<void> runFuture_;
|
||||
util::StopHelper stopHelper_;
|
||||
|
||||
static constexpr std::chrono::seconds kWS_TIMEOUT{30};
|
||||
static constexpr std::chrono::seconds kRETRY_MAX_DELAY{30};
|
||||
@@ -124,13 +125,6 @@ public:
|
||||
std::chrono::steady_clock::duration const retryDelay = SubscriptionSource::kRETRY_DELAY
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Subscription Source object
|
||||
*
|
||||
* @note This will block to wait for all the async operations to complete. io_context must be still running
|
||||
*/
|
||||
~SubscriptionSource();
|
||||
|
||||
/**
|
||||
* @brief Run the source
|
||||
*/
|
||||
@@ -192,7 +186,7 @@ public:
|
||||
* @brief Stop the source. The source will complete already scheduled operations but will not schedule new ones
|
||||
*/
|
||||
void
|
||||
stop();
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
private:
|
||||
void
|
||||
|
||||
@@ -115,6 +115,15 @@ public:
|
||||
* @brief Destructor of the SubscriptionManager object. It will block until all running jobs finished.
|
||||
*/
|
||||
~SubscriptionManager() override
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop the SubscriptionManager and wait for all jobs to finish.
|
||||
*/
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
ctx_.stop();
|
||||
ctx_.join();
|
||||
|
||||
@@ -45,6 +45,12 @@ class SubscriptionManagerInterface {
|
||||
public:
|
||||
virtual ~SubscriptionManagerInterface() = default;
|
||||
|
||||
/**
|
||||
* @brief Stop the SubscriptionManager and wait for all jobs to finish.
|
||||
*/
|
||||
virtual void
|
||||
stop() = 0;
|
||||
|
||||
/**
|
||||
* @brief Subscribe to the book changes feed.
|
||||
* @param subscriber
|
||||
|
||||
@@ -22,6 +22,7 @@ target_sources(
|
||||
requests/impl/SslContext.cpp
|
||||
ResponseExpirationCache.cpp
|
||||
SignalsHandler.cpp
|
||||
StopHelper.cpp
|
||||
Taggable.cpp
|
||||
TerminationHandler.cpp
|
||||
TimeUtils.cpp
|
||||
|
||||
46
src/util/StopHelper.cpp
Normal file
46
src/util/StopHelper.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/StopHelper.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace util {
|
||||
|
||||
void
|
||||
StopHelper::readyToStop()
|
||||
{
|
||||
onStopReady_();
|
||||
*stopped_ = true;
|
||||
}
|
||||
|
||||
void
|
||||
StopHelper::asyncWaitForStop(boost::asio::yield_context yield)
|
||||
{
|
||||
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::steady_clock::duration::max()};
|
||||
onStopReady_.connect([&timer]() { timer.cancel(); });
|
||||
boost::system::error_code error;
|
||||
if (!*stopped_)
|
||||
timer.async_wait(yield[error]);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
54
src/util/StopHelper.hpp
Normal file
54
src/util/StopHelper.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/asio/spawn.hpp>
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* @brief Helper class to stop a class asynchronously.
|
||||
*/
|
||||
class StopHelper {
|
||||
boost::signals2::signal<void()> onStopReady_;
|
||||
std::unique_ptr<std::atomic_bool> stopped_ = std::make_unique<std::atomic_bool>(false);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Notify that the class is ready to stop.
|
||||
*/
|
||||
void
|
||||
readyToStop();
|
||||
|
||||
/**
|
||||
* @brief Wait for the class to stop.
|
||||
*
|
||||
* @param yield The coroutine context
|
||||
*/
|
||||
void
|
||||
asyncWaitForStop(boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
@@ -42,6 +43,7 @@
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
@@ -120,7 +122,7 @@ detectSsl(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield)
|
||||
return SslDetectionResult{.socket = tcpStream.release_socket(), .isSsl = isSsl, .buffer = std::move(buffer)};
|
||||
}
|
||||
|
||||
std::expected<ConnectionPtr, std::optional<std::string>>
|
||||
std::expected<impl::UpgradableConnectionPtr, std::optional<std::string>>
|
||||
makeConnection(
|
||||
SslDetectionResult sslDetectionResult,
|
||||
std::optional<boost::asio::ssl::context>& sslContext,
|
||||
@@ -133,7 +135,7 @@ makeConnection(
|
||||
impl::UpgradableConnectionPtr connection;
|
||||
if (sslDetectionResult.isSsl) {
|
||||
if (not sslContext.has_value())
|
||||
return std::unexpected{"SSL is not supported by this server"};
|
||||
return std::unexpected{"Error creating a connection: SSL is not supported by this server"};
|
||||
|
||||
connection = std::make_unique<impl::SslHttpConnection>(
|
||||
std::move(sslDetectionResult.socket),
|
||||
@@ -157,7 +159,17 @@ makeConnection(
|
||||
connection->close(yield);
|
||||
return std::unexpected{std::nullopt};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
std::expected<ConnectionPtr, std::string>
|
||||
tryUpgradeConnection(
|
||||
impl::UpgradableConnectionPtr connection,
|
||||
std::optional<boost::asio::ssl::context>& sslContext,
|
||||
util::TagDecoratorFactory& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto const expectedIsUpgrade = connection->isUpgradeRequested(yield);
|
||||
if (not expectedIsUpgrade.has_value()) {
|
||||
return std::unexpected{
|
||||
@@ -256,8 +268,9 @@ Server::run()
|
||||
}
|
||||
|
||||
void
|
||||
Server::stop()
|
||||
Server::stop(boost::asio::yield_context yield)
|
||||
{
|
||||
connectionHandler_.stop(yield);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -288,15 +301,32 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield
|
||||
);
|
||||
if (not connectionExpected.has_value()) {
|
||||
if (connectionExpected.error().has_value()) {
|
||||
LOG(log_.info()) << "Error creating a connection: " << *connectionExpected.error();
|
||||
LOG(log_.info()) << *connectionExpected.error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOG(log_.trace()) << connectionExpected.value()->tag() << "Connection created";
|
||||
|
||||
if (connectionHandler_.isStopping()) {
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) {
|
||||
web::ng::impl::ConnectionHandler::stopConnection(*connection, yield);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
auto connection =
|
||||
tryUpgradeConnection(std::move(connectionExpected).value(), sslContext_, tagDecoratorFactory_, yield);
|
||||
if (not connection.has_value()) {
|
||||
LOG(log_.info()) << connection.error();
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[this, connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) mutable {
|
||||
[this, connection = std::move(connection).value()](boost::asio::yield_context yield) mutable {
|
||||
connectionHandler_.processConnection(std::move(connection), yield);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -40,10 +41,20 @@
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief A tag class for server to help identify Server in templated code.
|
||||
*/
|
||||
struct ServerTag {
|
||||
virtual ~ServerTag() = default;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept SomeServer = std::derived_from<T, ServerTag>;
|
||||
|
||||
/**
|
||||
* @brief Web server class.
|
||||
*/
|
||||
class Server {
|
||||
class Server : public ServerTag {
|
||||
public:
|
||||
/**
|
||||
* @brief Check to perform for each new client connection. The check takes client ip as input and returns a Response
|
||||
@@ -147,11 +158,13 @@ public:
|
||||
run();
|
||||
|
||||
/**
|
||||
* @brief Stop the server.
|
||||
** @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
|
||||
* @brief Stop the server. This method will asynchronously sleep unless all the users are disconnected.
|
||||
* @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
|
||||
*
|
||||
* @param yield The coroutine context.
|
||||
*/
|
||||
void
|
||||
stop();
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
private:
|
||||
void
|
||||
|
||||
@@ -35,10 +35,13 @@
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -138,8 +141,23 @@ ConnectionHandler::onWs(MessageHandler handler)
|
||||
void
|
||||
ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::yield_context yield)
|
||||
{
|
||||
LOG(log_.trace()) << connectionPtr->tag() << "New connection";
|
||||
auto& connectionRef = *connectionPtr;
|
||||
auto signalConnection = onStop_.connect([&connectionRef, yield]() { connectionRef.close(yield); });
|
||||
|
||||
if (isStopping()) {
|
||||
stopConnection(connectionRef, yield);
|
||||
return;
|
||||
}
|
||||
++connectionsCounter_.get();
|
||||
|
||||
// Using coroutine group here to wait for stopConnection() to finish before exiting this function and destroying
|
||||
// connection.
|
||||
util::CoroutineGroup stopTask{yield, 1};
|
||||
auto stopSignalConnection = onStop_.connect([&connectionRef, &stopTask, yield]() {
|
||||
stopTask.spawn(yield, [&connectionRef](boost::asio::yield_context innerYield) {
|
||||
stopConnection(connectionRef, innerYield);
|
||||
});
|
||||
});
|
||||
|
||||
bool shouldCloseGracefully = false;
|
||||
|
||||
@@ -173,21 +191,57 @@ ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::y
|
||||
}
|
||||
|
||||
if (shouldCloseGracefully) {
|
||||
connectionRef.setTimeout(kCLOSE_CONNECTION_TIMEOUT);
|
||||
connectionRef.close(yield);
|
||||
LOG(log_.trace()) << connectionRef.tag() << "Closed gracefully";
|
||||
}
|
||||
|
||||
signalConnection.disconnect();
|
||||
stopSignalConnection.disconnect();
|
||||
LOG(log_.trace()) << connectionRef.tag() << "Signal disconnected";
|
||||
|
||||
onDisconnectHook_(connectionRef);
|
||||
LOG(log_.trace()) << connectionRef.tag() << "Processing finished";
|
||||
|
||||
// Wait for a stopConnection() to finish if there is any to not have dangling reference in stopConnection().
|
||||
stopTask.asyncWait(yield);
|
||||
|
||||
--connectionsCounter_.get();
|
||||
if (connectionsCounter_.get().value() == 0 && stopping_)
|
||||
stopHelper_.readyToStop();
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::stop()
|
||||
ConnectionHandler::stopConnection(Connection& connection, boost::asio::yield_context yield)
|
||||
{
|
||||
util::Logger log{"WebServer"};
|
||||
LOG(log.trace()) << connection.tag() << "Stopping connection";
|
||||
Response response{
|
||||
boost::beast::http::status::service_unavailable,
|
||||
"This Clio node is shutting down. Please try another node.",
|
||||
connection
|
||||
};
|
||||
connection.send(std::move(response), yield);
|
||||
connection.setTimeout(kCLOSE_CONNECTION_TIMEOUT);
|
||||
connection.close(yield);
|
||||
LOG(log.trace()) << connection.tag() << "Connection closed";
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::stop(boost::asio::yield_context yield)
|
||||
{
|
||||
*stopping_ = true;
|
||||
onStop_();
|
||||
if (connectionsCounter_.get().value() == 0)
|
||||
return;
|
||||
|
||||
// Wait for server to disconnect all the users
|
||||
stopHelper_.asyncWaitForStop(yield);
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::isStopping() const
|
||||
{
|
||||
return *stopping_;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -211,7 +265,7 @@ ConnectionHandler::handleError(Error const& error, Connection const& connection)
|
||||
// Therefore, if we see a short read here, it has occurred
|
||||
// after the message has been completed, so it is safe to ignore it.
|
||||
if (error == boost::beast::http::error::end_of_stream || error == boost::asio::ssl::error::stream_truncated ||
|
||||
error == boost::asio::error::eof)
|
||||
error == boost::asio::error::eof || error == boost::beast::error::timeout)
|
||||
return false;
|
||||
|
||||
// WebSocket connection was gracefully closed
|
||||
@@ -308,7 +362,10 @@ ConnectionHandler::parallelRequestResponseLoop(
|
||||
);
|
||||
}
|
||||
}
|
||||
LOG(log_.trace()) << connection.tag()
|
||||
<< "Waiting processing tasks to finish. Number of tasks: " << tasksGroup.size();
|
||||
tasksGroup.asyncWait(yield);
|
||||
LOG(log_.trace()) << connection.tag() << "Processing is done";
|
||||
return closeConnectionGracefully;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/StopHelper.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "util/prometheus/Label.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
@@ -33,8 +37,11 @@
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -77,6 +84,12 @@ private:
|
||||
std::optional<MessageHandler> wsHandler_;
|
||||
|
||||
boost::signals2::signal<void()> onStop_;
|
||||
std::unique_ptr<std::atomic_bool> stopping_ = std::make_unique<std::atomic_bool>(false);
|
||||
|
||||
std::reference_wrapper<util::prometheus::GaugeInt> connectionsCounter_ =
|
||||
PrometheusService::gaugeInt("connections_total_number", util::prometheus::Labels{{{"status", "connected"}}});
|
||||
|
||||
util::StopHelper stopHelper_;
|
||||
|
||||
public:
|
||||
ConnectionHandler(
|
||||
@@ -87,6 +100,8 @@ public:
|
||||
OnDisconnectHook onDisconnectHook
|
||||
);
|
||||
|
||||
static constexpr std::chrono::milliseconds kCLOSE_CONNECTION_TIMEOUT{500};
|
||||
|
||||
void
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
|
||||
@@ -99,8 +114,14 @@ public:
|
||||
void
|
||||
processConnection(ConnectionPtr connection, boost::asio::yield_context yield);
|
||||
|
||||
static void
|
||||
stopConnection(Connection& connection, boost::asio::yield_context yield);
|
||||
|
||||
void
|
||||
stop();
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
bool
|
||||
isStopping() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -77,6 +77,7 @@ class HttpConnection : public UpgradableConnection {
|
||||
StreamType stream_;
|
||||
std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
|
||||
std::chrono::steady_clock::duration timeout_{kDEFAULT_TIMEOUT};
|
||||
bool closed_{false};
|
||||
|
||||
public:
|
||||
HttpConnection(
|
||||
@@ -152,6 +153,13 @@ public:
|
||||
void
|
||||
close(boost::asio::yield_context yield) override
|
||||
{
|
||||
// This is needed because calling async_shutdown() multiple times may lead to hanging coroutines.
|
||||
// See WsConnection for more details.
|
||||
if (closed_)
|
||||
return;
|
||||
|
||||
closed_ = true;
|
||||
|
||||
[[maybe_unused]] boost::system::error_code error;
|
||||
if constexpr (IsSslTcpStream<StreamType>) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
|
||||
|
||||
@@ -64,6 +64,7 @@ template <typename StreamType>
|
||||
class WsConnection : public WsConnectionBase {
|
||||
boost::beast::websocket::stream<StreamType> stream_;
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest_;
|
||||
bool closed_{false};
|
||||
|
||||
public:
|
||||
WsConnection(
|
||||
@@ -159,6 +160,13 @@ public:
|
||||
void
|
||||
close(boost::asio::yield_context yield) override
|
||||
{
|
||||
if (closed_)
|
||||
return;
|
||||
|
||||
// This should be set before the async_close(). Otherwise there is a possibility to have multiple coroutines
|
||||
// waiting on async_close(), but only one will be woken up after the actual close happened, others will hang.
|
||||
closed_ = true;
|
||||
|
||||
boost::system::error_code error; // unused
|
||||
stream_.async_close(boost::beast::websocket::close_code::normal, yield[error]);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,14 @@
|
||||
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
@@ -94,6 +98,38 @@ struct SyncAsioContextTest : virtual public NoLoggerFixture {
|
||||
runContext();
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void
|
||||
runSpawnWithTimeout(std::chrono::steady_clock::duration timeout, F&& f, bool allowMockLeak = false)
|
||||
{
|
||||
using namespace boost::asio;
|
||||
|
||||
boost::asio::io_context timerCtx;
|
||||
steady_timer timer{timerCtx, timeout};
|
||||
spawn(timerCtx, [this, &timer](yield_context yield) {
|
||||
boost::system::error_code errorCode;
|
||||
timer.async_wait(yield[errorCode]);
|
||||
ctx_.stop();
|
||||
EXPECT_TRUE(false) << "Test timed out";
|
||||
});
|
||||
std::thread timerThread{[&timerCtx]() { timerCtx.run(); }};
|
||||
|
||||
testing::MockFunction<void()> call;
|
||||
if (allowMockLeak)
|
||||
testing::Mock::AllowLeak(&call);
|
||||
|
||||
spawn(ctx_, [&](yield_context yield) {
|
||||
f(yield);
|
||||
call.Call();
|
||||
});
|
||||
|
||||
EXPECT_CALL(call, Call());
|
||||
runContext();
|
||||
|
||||
timerCtx.stop();
|
||||
timerThread.join();
|
||||
}
|
||||
|
||||
void
|
||||
runContext()
|
||||
{
|
||||
@@ -108,6 +144,15 @@ struct SyncAsioContextTest : virtual public NoLoggerFixture {
|
||||
ctx_.reset();
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
static void
|
||||
runSyncOperation(F&& f)
|
||||
{
|
||||
boost::asio::io_service ioc;
|
||||
boost::asio::spawn(ioc, f);
|
||||
ioc.run();
|
||||
}
|
||||
|
||||
protected:
|
||||
boost::asio::io_context ctx_;
|
||||
};
|
||||
|
||||
@@ -211,6 +211,8 @@ struct MockBackend : public BackendInterface {
|
||||
|
||||
MOCK_METHOD(void, doWriteLedgerObject, (std::string&&, std::uint32_t const, std::string&&), (override));
|
||||
|
||||
MOCK_METHOD(void, waitForWritesToFinish, (), (override));
|
||||
|
||||
MOCK_METHOD(bool, doFinishWrites, (), (override));
|
||||
|
||||
MOCK_METHOD(void, writeMPTHolders, (std::vector<MPTHolderData> const&), (override));
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "etl/Source.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/newconfig/ConfigDefinition.hpp"
|
||||
#include "util/newconfig/ObjectView.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -49,6 +48,7 @@
|
||||
|
||||
struct MockSource : etl::SourceBase {
|
||||
MOCK_METHOD(void, run, (), (override));
|
||||
MOCK_METHOD(void, stop, (boost::asio::yield_context), (override));
|
||||
MOCK_METHOD(bool, isConnected, (), (const, override));
|
||||
MOCK_METHOD(void, setForwarding, (bool), (override));
|
||||
MOCK_METHOD(boost::json::object, toJson, (), (const, override));
|
||||
@@ -89,6 +89,12 @@ public:
|
||||
mock_->run();
|
||||
}
|
||||
|
||||
void
|
||||
stop(boost::asio::yield_context yield) override
|
||||
{
|
||||
mock_->stop(yield);
|
||||
}
|
||||
|
||||
bool
|
||||
isConnected() const override
|
||||
{
|
||||
|
||||
@@ -102,6 +102,8 @@ struct MockSubscriptionManager : feed::SubscriptionManagerInterface {
|
||||
MOCK_METHOD(void, unsubProposedTransactions, (feed::SubscriberSharedPtr const&), (override));
|
||||
|
||||
MOCK_METHOD(boost::json::object, report, (), (const, override));
|
||||
|
||||
MOCK_METHOD(void, stop, (), (override));
|
||||
};
|
||||
|
||||
template <template <typename> typename MockType = ::testing::NiceMock>
|
||||
|
||||
@@ -54,7 +54,7 @@ struct MockConnectionImpl : web::ng::Connection {
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context));
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
};
|
||||
|
||||
using MockConnection = testing::NiceMock<MockConnectionImpl>;
|
||||
|
||||
@@ -57,7 +57,7 @@ struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context));
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
|
||||
using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
|
||||
MOCK_METHOD(IsUpgradeRequestedReturnType, isUpgradeRequested, (boost::asio::yield_context), (override));
|
||||
|
||||
@@ -47,7 +47,7 @@ struct MockWsConnectionImpl : web::ng::impl::WsConnectionBase {
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context));
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
|
||||
using SendBufferReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(SendBufferReturnType, sendBuffer, (boost::asio::const_buffer, boost::asio::yield_context), (override));
|
||||
|
||||
@@ -5,6 +5,7 @@ target_sources(
|
||||
PRIVATE # Common
|
||||
ConfigTests.cpp
|
||||
app/CliArgsTests.cpp
|
||||
app/StopperTests.cpp
|
||||
app/VerifyConfigTests.cpp
|
||||
app/WebHandlersTests.cpp
|
||||
data/AmendmentCenterTests.cpp
|
||||
@@ -151,6 +152,7 @@ target_sources(
|
||||
util/RepeatTests.cpp
|
||||
util/ResponseExpirationCacheTests.cpp
|
||||
util/SignalsHandlerTests.cpp
|
||||
util/StopHelperTests.cpp
|
||||
util/TimeUtilsTests.cpp
|
||||
util/TxUtilTests.cpp
|
||||
util/WithTimeout.cpp
|
||||
|
||||
116
tests/unit/app/StopperTests.cpp
Normal file
116
tests/unit/app/StopperTests.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, 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 "app/Stopper.hpp"
|
||||
#include "etl/ETLService.hpp"
|
||||
#include "etl/LoadBalancer.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/MockBackend.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockSubscriptionManager.hpp"
|
||||
#include "util/newconfig/ConfigDefinition.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
using namespace app;
|
||||
|
||||
struct StopperTest : NoLoggerFixture {
|
||||
protected:
|
||||
// Order here is important, stopper_ should die before mockCallback_, otherwise UB
|
||||
testing::StrictMock<testing::MockFunction<void(boost::asio::yield_context)>> mockCallback_;
|
||||
Stopper stopper_;
|
||||
};
|
||||
|
||||
TEST_F(StopperTest, stopCallsCallback)
|
||||
{
|
||||
stopper_.setOnStop(mockCallback_.AsStdFunction());
|
||||
EXPECT_CALL(mockCallback_, Call);
|
||||
stopper_.stop();
|
||||
}
|
||||
|
||||
TEST_F(StopperTest, stopCalledMultipleTimes)
|
||||
{
|
||||
stopper_.setOnStop(mockCallback_.AsStdFunction());
|
||||
EXPECT_CALL(mockCallback_, Call);
|
||||
stopper_.stop();
|
||||
stopper_.stop();
|
||||
stopper_.stop();
|
||||
stopper_.stop();
|
||||
}
|
||||
|
||||
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
|
||||
struct ServerMock : web::ng::ServerTag {
|
||||
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
|
||||
};
|
||||
struct LoadBalancerMock : etl::LoadBalancerTag {
|
||||
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
|
||||
};
|
||||
struct ETLServiceMock : etl::ETLServiceTag {
|
||||
MOCK_METHOD(void, stop, (), ());
|
||||
};
|
||||
|
||||
protected:
|
||||
testing::StrictMock<ServerMock> serverMock_;
|
||||
testing::StrictMock<LoadBalancerMock> loadBalancerMock_;
|
||||
testing::StrictMock<ETLServiceMock> etlServiceMock_;
|
||||
testing::StrictMock<MockSubscriptionManager> subscriptionManagerMock_;
|
||||
testing::StrictMock<MockBackend> backendMock_{util::config::ClioConfigDefinition{}};
|
||||
boost::asio::io_context ioContextToStop_;
|
||||
|
||||
bool
|
||||
isContextStopped() const
|
||||
{
|
||||
return ioContextToStop_.stopped();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(StopperMakeCallbackTest, makeCallbackTest)
|
||||
{
|
||||
auto contextWorkGuard = boost::asio::make_work_guard(ioContextToStop_);
|
||||
std::thread t{[this]() { ioContextToStop_.run(); }};
|
||||
|
||||
auto callback = Stopper::makeOnStopCallback(
|
||||
serverMock_, loadBalancerMock_, etlServiceMock_, subscriptionManagerMock_, backendMock_, ioContextToStop_
|
||||
);
|
||||
|
||||
testing::Sequence s1, s2;
|
||||
EXPECT_CALL(serverMock_, stop).InSequence(s1).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
|
||||
EXPECT_CALL(loadBalancerMock_, stop).InSequence(s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
|
||||
EXPECT_CALL(etlServiceMock_, stop).InSequence(s1, s2).WillOnce([this]() { EXPECT_FALSE(isContextStopped()); });
|
||||
EXPECT_CALL(subscriptionManagerMock_, stop).InSequence(s1, s2).WillOnce([this]() {
|
||||
EXPECT_FALSE(isContextStopped());
|
||||
});
|
||||
EXPECT_CALL(backendMock_, waitForWritesToFinish).InSequence(s1, s2).WillOnce([this]() {
|
||||
EXPECT_FALSE(isContextStopped());
|
||||
});
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
callback(yield);
|
||||
EXPECT_TRUE(isContextStopped());
|
||||
});
|
||||
|
||||
t.join();
|
||||
}
|
||||
@@ -318,6 +318,15 @@ TEST_F(LoadBalancerOnConnectHookTests, sourcesConnect_BothSourcesAreNotConnected
|
||||
sourceFactory_.callbacksAt(0).onConnect();
|
||||
}
|
||||
|
||||
struct LoadBalancerStopTests : LoadBalancerOnConnectHookTests, SyncAsioContextTest {};
|
||||
|
||||
TEST_F(LoadBalancerStopTests, stopCallsSourcesStop)
|
||||
{
|
||||
EXPECT_CALL(sourceFactory_.sourceAt(0), stop);
|
||||
EXPECT_CALL(sourceFactory_.sourceAt(1), stop);
|
||||
runSyncOperation([this](boost::asio::yield_context yield) { loadBalancer_->stop(yield); });
|
||||
}
|
||||
|
||||
struct LoadBalancerOnDisconnectHookTests : LoadBalancerOnConnectHookTests {
|
||||
LoadBalancerOnDisconnectHookTests()
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ struct SubscriptionSourceMock {
|
||||
MOCK_METHOD(void, setForwarding, (bool));
|
||||
MOCK_METHOD(std::chrono::steady_clock::time_point, lastMessageTime, (), (const));
|
||||
MOCK_METHOD(std::string, validatedRange, (), (const));
|
||||
MOCK_METHOD(void, stop, ());
|
||||
MOCK_METHOD(void, stop, (boost::asio::yield_context));
|
||||
};
|
||||
|
||||
struct ForwardingSourceMock {
|
||||
@@ -103,6 +103,14 @@ TEST_F(SourceImplTest, run)
|
||||
source_.run();
|
||||
}
|
||||
|
||||
TEST_F(SourceImplTest, stop)
|
||||
{
|
||||
EXPECT_CALL(*subscriptionSourceMock_, stop);
|
||||
boost::asio::io_context ctx;
|
||||
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { source_.stop(yield); });
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SourceImplTest, isConnected)
|
||||
{
|
||||
EXPECT_CALL(*subscriptionSourceMock_, isConnected()).WillOnce(testing::Return(true));
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include "etl/impl/SubscriptionSource.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockNetworkValidatedLedgers.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockSubscriptionManager.hpp"
|
||||
@@ -45,12 +45,18 @@ using namespace etl::impl;
|
||||
using testing::MockFunction;
|
||||
using testing::StrictMock;
|
||||
|
||||
struct SubscriptionSourceConnectionTestsBase : public NoLoggerFixture {
|
||||
struct SubscriptionSourceConnectionTestsBase : SyncAsioContextTest {
|
||||
SubscriptionSourceConnectionTestsBase()
|
||||
{
|
||||
subscriptionSource_.run();
|
||||
}
|
||||
|
||||
void
|
||||
stopSubscriptionSource()
|
||||
{
|
||||
boost::asio::spawn(ctx_, [this](auto&& yield) { subscriptionSource_.stop(yield); });
|
||||
}
|
||||
|
||||
[[maybe_unused]] TestWsConnection
|
||||
serverConnection(boost::asio::yield_context yield)
|
||||
{
|
||||
@@ -73,8 +79,7 @@ struct SubscriptionSourceConnectionTestsBase : public NoLoggerFixture {
|
||||
}
|
||||
|
||||
protected:
|
||||
boost::asio::io_context ioContext_;
|
||||
TestWsServer wsServer_{ioContext_, "0.0.0.0"};
|
||||
TestWsServer wsServer_{ctx_, "0.0.0.0"};
|
||||
|
||||
StrictMockNetworkValidatedLedgersPtr networkValidatedLedgers_;
|
||||
StrictMockSubscriptionManagerSharedPtr subscriptionManager_;
|
||||
@@ -84,7 +89,7 @@ protected:
|
||||
StrictMock<MockFunction<void()>> onLedgerClosedHook_;
|
||||
|
||||
SubscriptionSource subscriptionSource_{
|
||||
ioContext_,
|
||||
ctx_,
|
||||
"127.0.0.1",
|
||||
wsServer_.port(),
|
||||
networkValidatedLedgers_,
|
||||
@@ -101,43 +106,43 @@ struct SubscriptionSourceConnectionTests : util::prometheus::WithPrometheus, Sub
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, ConnectionFailed)
|
||||
{
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, ConnectionFailed_Retry_ConnectionFailed)
|
||||
{
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, ReadError)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = serverConnection(yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, ReadTimeout)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = serverConnection(yield);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, ReadError_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto connection = serverConnection(yield);
|
||||
connection.close(yield);
|
||||
@@ -145,14 +150,14 @@ TEST_F(SubscriptionSourceConnectionTests, ReadError_Reconnect)
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceConnectionTests, IsConnected)
|
||||
{
|
||||
EXPECT_FALSE(subscriptionSource_.isConnected());
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = serverConnection(yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
@@ -160,9 +165,9 @@ TEST_F(SubscriptionSourceConnectionTests, IsConnected)
|
||||
EXPECT_CALL(onConnectHook_, Call()).WillOnce([this]() { EXPECT_TRUE(subscriptionSource_.isConnected()); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() {
|
||||
EXPECT_FALSE(subscriptionSource_.isConnected());
|
||||
subscriptionSource_.stop();
|
||||
stopSubscriptionSource();
|
||||
});
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
struct SubscriptionSourceReadTestsBase : public SubscriptionSourceConnectionTestsBase {
|
||||
@@ -180,7 +185,7 @@ struct SubscriptionSourceReadTests : util::prometheus::WithPrometheus, Subscript
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotWrongMessage_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage("something", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -188,38 +193,38 @@ TEST_F(SubscriptionSourceReadTests, GotWrongMessage_Reconnect)
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResult)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndex)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"ledger_index":123}})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*networkValidatedLedgers_, push(123));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAsString_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"ledger_index":"123"}})", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -227,13 +232,13 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAsString_Reconnect)
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersAsNumber_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":123}})", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -241,8 +246,8 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersAsNumber_Reconn
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgers)
|
||||
@@ -257,14 +262,14 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgers)
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(789));
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(790));
|
||||
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":"123-456,789,32"}})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
|
||||
EXPECT_TRUE(subscriptionSource_.hasLedger(123));
|
||||
EXPECT_TRUE(subscriptionSource_.hasLedger(124));
|
||||
@@ -281,7 +286,7 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgers)
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersWrongValue_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"validated_ledgers":"123-456-789,32"}})", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -289,8 +294,8 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithValidatedLedgersWrongValue_Reco
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAndValidatedLedgers)
|
||||
@@ -301,15 +306,15 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAndValidatedLedgers)
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(3));
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(4));
|
||||
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"result":{"ledger_index":123,"validated_ledgers":"1-3"}})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*networkValidatedLedgers_, push(123));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
|
||||
EXPECT_EQ(subscriptionSource_.validatedRange(), "1-3");
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(0));
|
||||
@@ -321,21 +326,21 @@ TEST_F(SubscriptionSourceReadTests, GotResultWithLedgerIndexAndValidatedLedgers)
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosed)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"ledgerClosed"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedForwardingIsSet)
|
||||
{
|
||||
subscriptionSource_.setForwarding(true);
|
||||
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type": "ledgerClosed"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
@@ -344,27 +349,27 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedForwardingIsSet)
|
||||
EXPECT_CALL(onLedgerClosedHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() {
|
||||
EXPECT_FALSE(subscriptionSource_.isForwarding());
|
||||
subscriptionSource_.stop();
|
||||
stopSubscriptionSource();
|
||||
});
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndex)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type": "ledgerClosed","ledger_index": 123})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*networkValidatedLedgers_, push(123));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAsString_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","ledger_index":"123"}})", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -372,13 +377,13 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAsString_Recon
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GorLedgerClosedWithValidatedLedgersAsNumber_Reconnect)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","validated_ledgers":123})", yield);
|
||||
// We have to schedule receiving to receive close frame and boost will handle it automatically
|
||||
connection.receive(yield);
|
||||
@@ -386,8 +391,8 @@ TEST_F(SubscriptionSourceReadTests, GorLedgerClosedWithValidatedLedgersAsNumber_
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call()).Times(2);
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([]() {}).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithValidatedLedgers)
|
||||
@@ -397,14 +402,14 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithValidatedLedgers)
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(2));
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(3));
|
||||
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"ledgerClosed","validated_ledgers":"1-2"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(0));
|
||||
EXPECT_TRUE(subscriptionSource_.hasLedger(1));
|
||||
@@ -420,16 +425,16 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAndValidatedLe
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(2));
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(3));
|
||||
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection =
|
||||
connectAndSendMessage(R"({"type":"ledgerClosed","ledger_index":123,"validated_ledgers":"1-2"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*networkValidatedLedgers_, push(123));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
|
||||
EXPECT_FALSE(subscriptionSource_.hasLedger(0));
|
||||
EXPECT_TRUE(subscriptionSource_.hasLedger(1));
|
||||
@@ -440,14 +445,14 @@ TEST_F(SubscriptionSourceReadTests, GotLedgerClosedWithLedgerIndexAndValidatedLe
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingFalse)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"transaction":"some_transaction_data"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingTrue)
|
||||
@@ -455,15 +460,15 @@ TEST_F(SubscriptionSourceReadTests, GotTransactionIsForwardingTrue)
|
||||
subscriptionSource_.setForwarding(true);
|
||||
boost::json::object const message = {{"transaction", "some_transaction_data"}};
|
||||
|
||||
boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [&message, this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(boost::json::serialize(message), yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*subscriptionManager_, forwardProposedTransaction(message));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotTransactionWithMetaIsForwardingFalse)
|
||||
@@ -471,27 +476,27 @@ TEST_F(SubscriptionSourceReadTests, GotTransactionWithMetaIsForwardingFalse)
|
||||
subscriptionSource_.setForwarding(true);
|
||||
boost::json::object const message = {{"transaction", "some_transaction_data"}, {"meta", "some_meta_data"}};
|
||||
|
||||
boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [&message, this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(boost::json::serialize(message), yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*subscriptionManager_, forwardProposedTransaction(message)).Times(0);
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingFalse)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"validationReceived"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingTrue)
|
||||
@@ -499,27 +504,27 @@ TEST_F(SubscriptionSourceReadTests, GotValidationReceivedIsForwardingTrue)
|
||||
subscriptionSource_.setForwarding(true);
|
||||
boost::json::object const message = {{"type", "validationReceived"}};
|
||||
|
||||
boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [&message, this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(boost::json::serialize(message), yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*subscriptionManager_, forwardValidation(message));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotManiefstReceivedIsForwardingFalse)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(R"({"type":"manifestReceived"})", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, GotManifestReceivedIsForwardingTrue)
|
||||
@@ -527,27 +532,27 @@ TEST_F(SubscriptionSourceReadTests, GotManifestReceivedIsForwardingTrue)
|
||||
subscriptionSource_.setForwarding(true);
|
||||
boost::json::object const message = {{"type", "manifestReceived"}};
|
||||
|
||||
boost::asio::spawn(ioContext_, [&message, this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [&message, this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage(boost::json::serialize(message), yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(true)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(*subscriptionManager_, forwardManifest(message));
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionSourceReadTests, LastMessageTime)
|
||||
{
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage("some_message", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
ioContext_.run();
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
runContext();
|
||||
|
||||
auto const actualLastTimeMessage = subscriptionSource_.lastMessageTime();
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
@@ -563,18 +568,18 @@ TEST_F(SubscriptionSourcePrometheusCounterTests, LastMessageTime)
|
||||
auto& lastMessageTimeMock = makeMock<util::prometheus::GaugeInt>(
|
||||
"subscription_source_last_message_time", fmt::format("{{source=\"127.0.0.1:{}\"}}", wsServer_.port())
|
||||
);
|
||||
boost::asio::spawn(ioContext_, [this](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto connection = connectAndSendMessage("some_message", yield);
|
||||
connection.close(yield);
|
||||
});
|
||||
|
||||
EXPECT_CALL(onConnectHook_, Call());
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { subscriptionSource_.stop(); });
|
||||
EXPECT_CALL(onDisconnectHook_, Call(false)).WillOnce([this]() { stopSubscriptionSource(); });
|
||||
EXPECT_CALL(lastMessageTimeMock, set).WillOnce([](int64_t value) {
|
||||
auto const now =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
EXPECT_LE(now - value, 1);
|
||||
});
|
||||
ioContext_.run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
62
tests/unit/util/StopHelperTests.cpp
Normal file
62
tests/unit/util/StopHelperTests.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/StopHelper.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
using namespace util;
|
||||
|
||||
struct StopHelperTests : SyncAsioContextTest {
|
||||
protected:
|
||||
StopHelper stopHelper_;
|
||||
testing::StrictMock<testing::MockFunction<void()>> readyToStopCalled_;
|
||||
testing::StrictMock<testing::MockFunction<void()>> asyncWaitForStopFinished_;
|
||||
};
|
||||
|
||||
TEST_F(StopHelperTests, asyncWaitForStopWaitsForReadyToStop)
|
||||
{
|
||||
testing::Sequence const sequence;
|
||||
EXPECT_CALL(readyToStopCalled_, Call).InSequence(sequence);
|
||||
EXPECT_CALL(asyncWaitForStopFinished_, Call).InSequence(sequence);
|
||||
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
stopHelper_.asyncWaitForStop(yield);
|
||||
asyncWaitForStopFinished_.Call();
|
||||
});
|
||||
|
||||
runSpawn([this](auto&&) {
|
||||
stopHelper_.readyToStop();
|
||||
readyToStopCalled_.Call();
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(StopHelperTests, readyToStopCalledBeforeAsyncWait)
|
||||
{
|
||||
stopHelper_.readyToStop();
|
||||
EXPECT_CALL(asyncWaitForStopFinished_, Call);
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
stopHelper_.asyncWaitForStop(yield);
|
||||
asyncWaitForStopFinished_.Call();
|
||||
});
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/AssignRandomPort.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/TestHttpClient.hpp"
|
||||
@@ -45,6 +46,7 @@
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
@@ -149,7 +151,7 @@ INSTANTIATE_TEST_CASE_P(
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
struct ServerTest : SyncAsioContextTest {
|
||||
struct ServerTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
|
||||
ServerTest()
|
||||
{
|
||||
[&]() { ASSERT_TRUE(server_.has_value()); }();
|
||||
@@ -421,6 +423,35 @@ TEST_F(ServerHttpTest, OnDisconnectHook)
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerHttpTest, ClientIsDisconnectedIfServerStopped)
|
||||
{
|
||||
HttpAsyncClient client{ctx_};
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
|
||||
maybeError = client.send(
|
||||
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
|
||||
yield,
|
||||
std::chrono::milliseconds{100}
|
||||
);
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
auto message = client.receive(yield, std::chrono::milliseconds{100});
|
||||
EXPECT_TRUE(message.has_value()) << message.error().message();
|
||||
EXPECT_EQ(message->result(), http::status::service_unavailable);
|
||||
EXPECT_EQ(message->body(), "This Clio node is shutting down. Please try another node.");
|
||||
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
runSyncOperation([this](auto yield) { server_->stop(yield); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_P(ServerHttpTest, RequestResponse)
|
||||
{
|
||||
HttpAsyncClient client{ctx_};
|
||||
@@ -533,3 +564,20 @@ TEST_F(ServerTest, WsRequestResponse)
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, WsClientIsDisconnectedIfServerStopped)
|
||||
{
|
||||
WebSocketAsyncClient client{ctx_};
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
EXPECT_TRUE(maybeError.has_value());
|
||||
EXPECT_EQ(maybeError.value().value(), static_cast<int>(boost::beast::websocket::error::upgrade_declined));
|
||||
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
runSyncOperation([this](auto yield) { server_->stop(yield); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/UnsupportedType.hpp"
|
||||
#include "util/newconfig/ConfigDefinition.hpp"
|
||||
@@ -64,7 +65,7 @@ namespace beast = boost::beast;
|
||||
namespace http = boost::beast::http;
|
||||
namespace websocket = boost::beast::websocket;
|
||||
|
||||
struct ConnectionHandlerTest : SyncAsioContextTest {
|
||||
struct ConnectionHandlerTest : prometheus::WithPrometheus, SyncAsioContextTest {
|
||||
ConnectionHandlerTest(ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
|
||||
: tagFactory{util::config::ClioConfigDefinition{
|
||||
{"log_tag_style", config::ConfigValue{config::ConfigType::String}.defaultValue("uint")}
|
||||
@@ -136,6 +137,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection)
|
||||
{
|
||||
EXPECT_CALL(*mockHttpConnection, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
|
||||
EXPECT_CALL(
|
||||
*mockHttpConnection,
|
||||
setTimeout(std::chrono::steady_clock::duration{ConnectionHandler::kCLOSE_CONNECTION_TIMEOUT})
|
||||
);
|
||||
EXPECT_CALL(*mockHttpConnection, close);
|
||||
EXPECT_CALL(onDisconnectMock, Call).WillOnce([connectionPtr = mockHttpConnection.get()](Connection const& c) {
|
||||
EXPECT_EQ(&c, connectionPtr);
|
||||
@@ -352,6 +357,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsNullForHt
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockHttpConnection,
|
||||
setTimeout(std::chrono::steady_clock::duration{ConnectionHandler::kCLOSE_CONNECTION_TIMEOUT})
|
||||
);
|
||||
EXPECT_CALL(*mockHttpConnection, close);
|
||||
|
||||
EXPECT_CALL(onDisconnectMock, Call).WillOnce([connectionPtr = mockHttpConnection.get()](Connection const& c) {
|
||||
@@ -394,6 +403,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockHttpConnection,
|
||||
setTimeout(std::chrono::steady_clock::duration{ConnectionHandler::kCLOSE_CONNECTION_TIMEOUT})
|
||||
);
|
||||
EXPECT_CALL(*mockHttpConnection, close);
|
||||
|
||||
EXPECT_CALL(onDisconnectMock, Call).WillOnce([connectionPtr = mockHttpConnection.get()](Connection const& c) {
|
||||
@@ -451,7 +464,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
std::string const responseMessage = "some response";
|
||||
bool connectionClosed = false;
|
||||
|
||||
EXPECT_CALL(*mockWsConnection, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection, wasUpgraded).Times(2).WillRepeatedly(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection, receive).Times(4).WillRepeatedly([&](auto&&) -> std::expected<Request, Error> {
|
||||
if (connectionClosed) {
|
||||
return makeError(websocket::error::closed);
|
||||
@@ -465,16 +478,33 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
});
|
||||
|
||||
size_t numCalls = 0;
|
||||
EXPECT_CALL(*mockWsConnection, send).Times(3).WillRepeatedly([&](Response response, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
EXPECT_CALL(
|
||||
*mockWsConnection,
|
||||
send(testing::ResultOf([](Response const& r) { return r.message(); }, responseMessage), testing::_)
|
||||
)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&](auto&&, auto&&) {
|
||||
++numCalls;
|
||||
if (numCalls == 3)
|
||||
boost::asio::spawn(ctx_, [this](auto yield) { connectionHandler.stop(yield); });
|
||||
|
||||
++numCalls;
|
||||
if (numCalls == 3)
|
||||
connectionHandler.stop();
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(
|
||||
*mockWsConnection,
|
||||
send(
|
||||
testing::ResultOf(
|
||||
[](Response const& r) { return r.message(); },
|
||||
"This Clio node is shutting down. Please try another node."
|
||||
),
|
||||
testing::_
|
||||
)
|
||||
);
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockWsConnection, setTimeout(std::chrono::steady_clock::duration{ConnectionHandler::kCLOSE_CONNECTION_TIMEOUT})
|
||||
);
|
||||
EXPECT_CALL(*mockWsConnection, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
|
||||
|
||||
EXPECT_CALL(onDisconnectMock, Call).WillOnce([connectionPtr = mockWsConnection.get()](Connection const& c) {
|
||||
@@ -486,6 +516,36 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, ProcessCalledAfterStop)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
runSyncOperation([this](boost::asio::yield_context yield) { connectionHandler.stop(yield); });
|
||||
|
||||
EXPECT_CALL(*mockWsConnection, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(
|
||||
*mockWsConnection,
|
||||
send(
|
||||
testing::ResultOf(
|
||||
[](Response const& r) { return r.message(); }, testing::HasSubstr("This Clio node is shutting down")
|
||||
),
|
||||
testing::_
|
||||
)
|
||||
);
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockWsConnection, setTimeout(std::chrono::steady_clock::duration{ConnectionHandler::kCLOSE_CONNECTION_TIMEOUT})
|
||||
);
|
||||
EXPECT_CALL(*mockWsConnection, close);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler.processConnection(std::move(mockWsConnection), yield);
|
||||
});
|
||||
}
|
||||
|
||||
struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
|
||||
static constexpr size_t kMAX_PARALLEL_REQUESTS = 3;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/TestHttpServer.hpp"
|
||||
#include "util/TestWebSocketClient.hpp"
|
||||
@@ -308,3 +309,28 @@ TEST_F(WebWsConnectionTests, CloseWhenConnectionIsAlreadyClosed)
|
||||
wsConnection->close(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WebWsConnectionTests, CloseCalledFromMultipleSubCoroutines)
|
||||
{
|
||||
boost::asio::spawn(ctx_, [this](boost::asio::yield_context yield) {
|
||||
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
|
||||
});
|
||||
|
||||
testing::StrictMock<testing::MockFunction<void()>> closeCalled;
|
||||
EXPECT_CALL(closeCalled, Call).Times(2);
|
||||
|
||||
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
|
||||
auto wsConnection = acceptConnection(yield);
|
||||
util::CoroutineGroup coroutines{yield};
|
||||
for ([[maybe_unused]] int i : std::ranges::iota_view{0, 2}) {
|
||||
coroutines.spawn(yield, [&wsConnection, &closeCalled](boost::asio::yield_context innerYield) {
|
||||
wsConnection->close(innerYield);
|
||||
closeCalled.Call();
|
||||
});
|
||||
}
|
||||
auto const receivedMessage = wsConnection->receive(yield);
|
||||
EXPECT_FALSE(receivedMessage.has_value());
|
||||
coroutines.asyncWait(yield);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user