Skip to content

Commit 9570286

Browse files
authored
feat: Graceful shutdown (#1801)
Fixes #442.
1 parent 12e6fcc commit 9570286

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1072
-190
lines changed

src/app/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
add_library(clio_app)
2-
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp WebHandlers.cpp)
2+
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp Stopper.cpp WebHandlers.cpp)
33

44
target_link_libraries(clio_app PUBLIC clio_etl clio_etlng clio_feed clio_web clio_rpc clio_migration)

src/app/ClioApplication.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "app/ClioApplication.hpp"
2121

22+
#include "app/Stopper.hpp"
2223
#include "app/WebHandlers.hpp"
2324
#include "data/AmendmentCenter.hpp"
2425
#include "data/BackendFactory.hpp"
@@ -45,6 +46,8 @@
4546
#include "web/ng/Server.hpp"
4647

4748
#include <boost/asio/io_context.hpp>
49+
#include <boost/asio/spawn.hpp>
50+
#include <boost/asio/use_future.hpp>
4851

4952
#include <cstdint>
5053
#include <cstdlib>
@@ -84,6 +87,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
8487
{
8588
LOG(util::LogService::info()) << "Clio version: " << util::build::getClioFullVersionString();
8689
PrometheusService::init(config);
90+
signalsHandler_.subscribeToStop([this]() { appStopper_.stop(); });
8791
}
8892

8993
int
@@ -169,6 +173,10 @@ ClioApplication::run(bool const useNgWebServer)
169173
return EXIT_FAILURE;
170174
}
171175

176+
appStopper_.setOnStop(
177+
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
178+
);
179+
172180
// Blocks until stopped.
173181
// When stopped, shared_ptrs fall out of scope
174182
// Calls destructors on all resources, and destructs in order

src/app/ClioApplication.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#pragma once
2121

22+
#include "app/Stopper.hpp"
2223
#include "util/SignalsHandler.hpp"
2324
#include "util/newconfig/ConfigDefinition.hpp"
2425

@@ -30,6 +31,7 @@ namespace app {
3031
class ClioApplication {
3132
util::config::ClioConfigDefinition const& config_;
3233
util::SignalsHandler signalsHandler_;
34+
Stopper appStopper_;
3335

3436
public:
3537
/**

src/app/Stopper.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//------------------------------------------------------------------------------
2+
/*
3+
This file is part of clio: https://github.com/XRPLF/clio
4+
Copyright (c) 2025, the clio developers.
5+
6+
Permission to use, copy, modify, and distribute this software for any
7+
purpose with or without fee is hereby granted, provided that the above
8+
copyright notice and this permission notice appear in all copies.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
//==============================================================================
19+
20+
#include "app/Stopper.hpp"
21+
22+
#include <boost/asio/spawn.hpp>
23+
24+
#include <functional>
25+
#include <thread>
26+
#include <utility>
27+
28+
namespace app {
29+
30+
Stopper::~Stopper()
31+
{
32+
if (worker_.joinable())
33+
worker_.join();
34+
}
35+
36+
void
37+
Stopper::setOnStop(std::function<void(boost::asio::yield_context)> cb)
38+
{
39+
boost::asio::spawn(ctx_, std::move(cb));
40+
}
41+
42+
void
43+
Stopper::stop()
44+
{
45+
// Do nothing if worker_ is already running
46+
if (worker_.joinable())
47+
return;
48+
49+
worker_ = std::thread{[this]() { ctx_.run(); }};
50+
}
51+
52+
} // namespace app

src/app/Stopper.hpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//------------------------------------------------------------------------------
2+
/*
3+
This file is part of clio: https://github.com/XRPLF/clio
4+
Copyright (c) 2024, the clio developers.
5+
6+
Permission to use, copy, modify, and distribute this software for any
7+
purpose with or without fee is hereby granted, provided that the above
8+
copyright notice and this permission notice appear in all copies.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
//==============================================================================
19+
20+
#pragma once
21+
22+
#include "data/BackendInterface.hpp"
23+
#include "etl/ETLService.hpp"
24+
#include "etl/LoadBalancer.hpp"
25+
#include "feed/SubscriptionManagerInterface.hpp"
26+
#include "util/CoroutineGroup.hpp"
27+
#include "util/log/Logger.hpp"
28+
#include "web/ng/Server.hpp"
29+
30+
#include <boost/asio/executor_work_guard.hpp>
31+
#include <boost/asio/io_context.hpp>
32+
#include <boost/asio/spawn.hpp>
33+
34+
#include <functional>
35+
#include <thread>
36+
37+
namespace app {
38+
39+
/**
40+
* @brief Application stopper class. On stop it will create a new thread to run all the shutdown tasks.
41+
*/
42+
class Stopper {
43+
boost::asio::io_context ctx_;
44+
std::thread worker_;
45+
46+
public:
47+
/**
48+
* @brief Destroy the Stopper object
49+
*/
50+
~Stopper();
51+
52+
/**
53+
* @brief Set the callback to be called when the application is stopped.
54+
*
55+
* @param cb The callback to be called on application stop.
56+
*/
57+
void
58+
setOnStop(std::function<void(boost::asio::yield_context)> cb);
59+
60+
/**
61+
* @brief Stop the application and run the shutdown tasks.
62+
*/
63+
void
64+
stop();
65+
66+
/**
67+
* @brief Create a callback to be called on application stop.
68+
*
69+
* @param server The server to stop.
70+
* @param balancer The load balancer to stop.
71+
* @param etl The ETL service to stop.
72+
* @param subscriptions The subscription manager to stop.
73+
* @param backend The backend to stop.
74+
* @param ioc The io_context to stop.
75+
* @return The callback to be called on application stop.
76+
*/
77+
template <
78+
web::ng::SomeServer ServerType,
79+
etl::SomeLoadBalancer LoadBalancerType,
80+
etl::SomeETLService ETLServiceType>
81+
static std::function<void(boost::asio::yield_context)>
82+
makeOnStopCallback(
83+
ServerType& server,
84+
LoadBalancerType& balancer,
85+
ETLServiceType& etl,
86+
feed::SubscriptionManagerInterface& subscriptions,
87+
data::BackendInterface& backend,
88+
boost::asio::io_context& ioc
89+
)
90+
{
91+
return [&](boost::asio::yield_context yield) {
92+
util::CoroutineGroup coroutineGroup{yield};
93+
coroutineGroup.spawn(yield, [&server](auto innerYield) {
94+
server.stop(innerYield);
95+
LOG(util::LogService::info()) << "Server stopped";
96+
});
97+
coroutineGroup.spawn(yield, [&balancer](auto innerYield) {
98+
balancer.stop(innerYield);
99+
LOG(util::LogService::info()) << "LoadBalancer stopped";
100+
});
101+
coroutineGroup.asyncWait(yield);
102+
103+
etl.stop();
104+
LOG(util::LogService::info()) << "ETL stopped";
105+
106+
subscriptions.stop();
107+
LOG(util::LogService::info()) << "SubscriptionManager stopped";
108+
109+
backend.waitForWritesToFinish();
110+
LOG(util::LogService::info()) << "Backend writes finished";
111+
112+
ioc.stop();
113+
LOG(util::LogService::info()) << "io_context stopped";
114+
};
115+
}
116+
};
117+
118+
} // namespace app

src/data/BackendInterface.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,12 @@ class BackendInterface {
683683
bool
684684
finishWrites(std::uint32_t ledgerSequence);
685685

686+
/**
687+
* @brief Wait for all pending writes to finish.
688+
*/
689+
virtual void
690+
waitForWritesToFinish() = 0;
691+
686692
/**
687693
* @brief Mark the migration status of a migrator as Migrated in the database
688694
*

src/data/CassandraBackend.hpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,16 @@ class BasicCassandraBackend : public BackendInterface {
188188
return {txns, {}};
189189
}
190190

191+
void
192+
waitForWritesToFinish() override
193+
{
194+
executor_.sync();
195+
}
196+
191197
bool
192198
doFinishWrites() override
193199
{
194-
// wait for other threads to finish their writes
195-
executor_.sync();
200+
waitForWritesToFinish();
196201

197202
if (!range_) {
198203
executor_.writeSync(schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_);

src/etl/ETLService.hpp

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
4343
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
4444

45+
#include <concepts>
4546
#include <cstddef>
4647
#include <cstdint>
4748
#include <memory>
@@ -58,6 +59,16 @@ struct NFTsData;
5859
*/
5960
namespace etl {
6061

62+
/**
63+
* @brief A tag class to help identify ETLService in templated code.
64+
*/
65+
struct ETLServiceTag {
66+
virtual ~ETLServiceTag() = default;
67+
};
68+
69+
template <typename T>
70+
concept SomeETLService = std::derived_from<T, ETLServiceTag>;
71+
6172
/**
6273
* @brief This class is responsible for continuously extracting data from a p2p node, and writing that data to the
6374
* databases.
@@ -71,7 +82,7 @@ namespace etl {
7182
* the others will fall back to monitoring/publishing. In this sense, this class dynamically transitions from monitoring
7283
* to writing and from writing to monitoring, based on the activity of other processes running on different machines.
7384
*/
74-
class ETLService {
85+
class ETLService : public ETLServiceTag {
7586
// TODO: make these template parameters in ETLService
7687
using LoadBalancerType = LoadBalancer;
7788
using DataPipeType = etl::impl::ExtractionDataPipe<org::xrpl::rpc::v1::GetLedgerResponse>;
@@ -159,10 +170,20 @@ class ETLService {
159170
/**
160171
* @brief Stops components and joins worker thread.
161172
*/
162-
~ETLService()
173+
~ETLService() override
174+
{
175+
if (not state_.isStopping)
176+
stop();
177+
}
178+
179+
/**
180+
* @brief Stop the ETL service.
181+
* @note This method blocks until the ETL service has stopped.
182+
*/
183+
void
184+
stop()
163185
{
164-
LOG(log_.info()) << "onStop called";
165-
LOG(log_.debug()) << "Stopping Reporting ETL";
186+
LOG(log_.info()) << "Stop called";
166187

167188
state_.isStopping = true;
168189
cacheLoader_.stop();

src/etl/LoadBalancer.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "feed/SubscriptionManagerInterface.hpp"
2727
#include "rpc/Errors.hpp"
2828
#include "util/Assert.hpp"
29+
#include "util/CoroutineGroup.hpp"
2930
#include "util/Random.hpp"
3031
#include "util/ResponseExpirationCache.hpp"
3132
#include "util/log/Logger.hpp"
@@ -336,6 +337,16 @@ LoadBalancer::getETLState() noexcept
336337
return etlState_;
337338
}
338339

340+
void
341+
LoadBalancer::stop(boost::asio::yield_context yield)
342+
{
343+
util::CoroutineGroup group{yield};
344+
std::ranges::for_each(sources_, [&group, yield](auto& source) {
345+
group.spawn(yield, [&source](boost::asio::yield_context innerYield) { source->stop(innerYield); });
346+
});
347+
group.asyncWait(yield);
348+
}
349+
339350
void
340351
LoadBalancer::chooseForwardingSource()
341352
{

0 commit comments

Comments
 (0)