Skip to content

Commit 346c9f9

Browse files
authored
feat: Read and write LedgerCache to file (#2761)
Fixes #2413.
1 parent c6308ce commit 346c9f9

35 files changed

+2725
-26
lines changed

docs/config-description.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,22 @@ This document provides a list of all available Clio configuration properties in
441441
- **Constraints**: The value must be one of the following: `sync`, `async`, `none`.
442442
- **Description**: The strategy used for Cache loading.
443443

444+
### cache.file.path
445+
446+
- **Required**: False
447+
- **Type**: string
448+
- **Default value**: None
449+
- **Constraints**: None
450+
- **Description**: The path to a file where cache will be saved to on shutdown and loaded from on startup. If the file couldn't be read Clio will load cache as usual (from DB or from rippled).
451+
452+
### cache.file.max_sequence_age
453+
454+
- **Required**: True
455+
- **Type**: int
456+
- **Default value**: `5000`
457+
- **Constraints**: None
458+
- **Description**: Max allowed difference between the latest sequence in DB and in cache file. If the cache file is too old (contains too low latest sequence) Clio will reject using it.
459+
444460
### log.channels.[].channel
445461

446462
- **Required**: False

docs/examples/config/example-config.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,11 @@
137137
// "num_cursors_from_account": 3200, // Read the cursors from the account table until we have enough cursors to partition the ledger to load concurrently.
138138
"num_markers": 48, // The number of markers is the number of coroutines to load the cache concurrently.
139139
"page_fetch_size": 512, // The number of rows to load for each page.
140-
"load": "async" // "sync" to load cache synchronously or "async" to load cache asynchronously or "none"/"no" to turn off the cache.
140+
"load": "async", // "sync" to load cache synchronously or "async" to load cache asynchronously or "none"/"no" to turn off the cache.
141+
"file": {
142+
"path": "./cache.bin",
143+
"max_sequence_age": 5000
144+
}
141145
},
142146
"prometheus": {
143147
"enabled": true,

src/app/ClioApplication.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include <cstdlib>
5656
#include <memory>
5757
#include <optional>
58+
#include <string>
5859
#include <thread>
5960
#include <utility>
6061
#include <vector>
@@ -110,7 +111,23 @@ ClioApplication::run(bool const useNgWebServer)
110111
auto const dosguardWeights = web::dosguard::Weights::make(config_);
111112
auto dosGuard = web::dosguard::DOSGuard{config_, whitelistHandler, dosguardWeights};
112113
auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard};
114+
113115
auto cache = data::LedgerCache{};
116+
appStopper_.setOnStop([&cache, this](auto&&) {
117+
// TODO(kuznetsss): move this into Stopper::makeOnStopCallback()
118+
auto const cacheFilePath = config_.maybeValue<std::string>("cache.file.path");
119+
if (not cacheFilePath.has_value()) {
120+
return;
121+
}
122+
123+
LOG(util::LogService::info()) << "Saving ledger cache to " << *cacheFilePath;
124+
if (auto const [success, duration_ms] = util::timed([&]() { return cache.saveToFile(*cacheFilePath); });
125+
success.has_value()) {
126+
LOG(util::LogService::info()) << "Successfully saved ledger cache in " << duration_ms << " ms";
127+
} else {
128+
LOG(util::LogService::error()) << "Error saving LedgerCache to file";
129+
}
130+
});
114131

115132
// Interface to the database
116133
auto backend = data::makeBackend(config_, cache);

src/data/BackendInterface.cpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ BackendInterface::updateRange(uint32_t newMax)
270270
{
271271
std::scoped_lock const lck(rngMtx_);
272272

273-
if (range_.has_value() && newMax < range_->maxSequence) {
273+
if (range_.has_value() and newMax < range_->maxSequence) {
274274
ASSERT(
275275
false,
276276
"Range shouldn't exist yet or newMax should be at least range->maxSequence. newMax = {}, "
@@ -280,11 +280,14 @@ BackendInterface::updateRange(uint32_t newMax)
280280
);
281281
}
282282

283-
if (!range_.has_value()) {
284-
range_ = {.minSequence = newMax, .maxSequence = newMax};
285-
} else {
286-
range_->maxSequence = newMax;
287-
}
283+
updateRangeImpl(newMax);
284+
}
285+
286+
void
287+
BackendInterface::forceUpdateRange(uint32_t newMax)
288+
{
289+
std::scoped_lock const lck(rngMtx_);
290+
updateRangeImpl(newMax);
288291
}
289292

290293
void
@@ -410,4 +413,14 @@ BackendInterface::fetchFees(std::uint32_t const seq, boost::asio::yield_context
410413
return fees;
411414
}
412415

416+
void
417+
BackendInterface::updateRangeImpl(uint32_t newMax)
418+
{
419+
if (!range_.has_value()) {
420+
range_ = {.minSequence = newMax, .maxSequence = newMax};
421+
} else {
422+
range_->maxSequence = newMax;
423+
}
424+
}
425+
413426
} // namespace data

src/data/BackendInterface.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,15 @@ class BackendInterface {
249249
void
250250
updateRange(uint32_t newMax);
251251

252+
/**
253+
* @brief Updates the range of sequences that are stored in the DB without any checks
254+
* @note In the most cases you should use updateRange() instead
255+
*
256+
* @param newMax The new maximum sequence available
257+
*/
258+
void
259+
forceUpdateRange(uint32_t newMax);
260+
252261
/**
253262
* @brief Sets the range of sequences that are stored in the DB.
254263
*
@@ -776,6 +785,9 @@ class BackendInterface {
776785
*/
777786
virtual bool
778787
doFinishWrites() = 0;
788+
789+
void
790+
updateRangeImpl(uint32_t newMax);
779791
};
780792

781793
} // namespace data

src/data/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ target_sources(
1414
cassandra/impl/SslContext.cpp
1515
cassandra/Handle.cpp
1616
cassandra/SettingsProvider.cpp
17+
impl/InputFile.cpp
18+
impl/LedgerCacheFile.cpp
19+
impl/OutputFile.cpp
1720
)
1821

1922
target_link_libraries(clio_data PUBLIC cassandra-cpp-driver::cassandra-cpp-driver clio_util)

src/data/LedgerCache.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@
2020
#include "data/LedgerCache.hpp"
2121

2222
#include "data/Types.hpp"
23+
#include "data/impl/LedgerCacheFile.hpp"
2324
#include "etl/Models.hpp"
2425
#include "util/Assert.hpp"
2526

2627
#include <xrpl/basics/base_uint.h>
2728

2829
#include <cstddef>
2930
#include <cstdint>
31+
#include <cstdlib>
32+
#include <cstring>
33+
#include <map>
3034
#include <mutex>
3135
#include <optional>
3236
#include <shared_mutex>
37+
#include <string>
38+
#include <utility>
3339
#include <vector>
3440

3541
namespace data {
@@ -251,4 +257,34 @@ LedgerCache::getSuccessorHitRate() const
251257
return static_cast<float>(successorHitCounter_.get().value()) / successorReqCounter_.get().value();
252258
}
253259

260+
std::expected<void, std::string>
261+
LedgerCache::saveToFile(std::string const& path) const
262+
{
263+
if (not isFull()) {
264+
return std::unexpected{"Ledger cache is not full"};
265+
}
266+
267+
impl::LedgerCacheFile file{path};
268+
std::unique_lock lock{mtx_};
269+
impl::LedgerCacheFile::DataView data{.latestSeq = latestSeq_, .map = map_, .deleted = deleted_};
270+
return file.write(data);
271+
}
272+
273+
std::expected<void, std::string>
274+
LedgerCache::loadFromFile(std::string const& path, uint32_t minLatestSequence)
275+
{
276+
impl::LedgerCacheFile file{path};
277+
auto data = file.read(minLatestSequence);
278+
if (not data.has_value()) {
279+
return std::unexpected(std::move(data).error());
280+
}
281+
auto [latestSeq, map, deleted] = std::move(data).value();
282+
std::unique_lock lock{mtx_};
283+
latestSeq_ = latestSeq;
284+
map_ = std::move(map);
285+
deleted_ = std::move(deleted);
286+
full_ = true;
287+
return {};
288+
}
289+
254290
} // namespace data

src/data/LedgerCache.hpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include <map>
3838
#include <optional>
3939
#include <shared_mutex>
40+
#include <string>
4041
#include <unordered_set>
4142
#include <vector>
4243

@@ -46,11 +47,16 @@ namespace data {
4647
* @brief Cache for an entire ledger.
4748
*/
4849
class LedgerCache : public LedgerCacheInterface {
50+
public:
51+
/** @brief An entry of the cache */
4952
struct CacheEntry {
5053
uint32_t seq = 0;
5154
Blob blob;
5255
};
5356

57+
using CacheMap = std::map<ripple::uint256, CacheEntry>;
58+
59+
private:
5460
// counters for fetchLedgerObject(s) hit rate
5561
std::reference_wrapper<util::prometheus::CounterInt> objectReqCounter_{PrometheusService::counterInt(
5662
"ledger_cache_counter_total_number",
@@ -73,8 +79,8 @@ class LedgerCache : public LedgerCacheInterface {
7379
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "successor_key"}})
7480
)};
7581

76-
std::map<ripple::uint256, CacheEntry> map_;
77-
std::map<ripple::uint256, CacheEntry> deleted_;
82+
CacheMap map_;
83+
CacheMap deleted_;
7884

7985
mutable std::shared_mutex mtx_;
8086
std::condition_variable_any cv_;
@@ -138,6 +144,19 @@ class LedgerCache : public LedgerCacheInterface {
138144

139145
void
140146
waitUntilCacheContainsSeq(uint32_t seq) override;
147+
148+
/**
149+
* @brief Save the cache to file
150+
* @note This operation takes about 7 seconds and it keeps mtx_ exclusively locked
151+
*
152+
* @param path The file path to save the cache to
153+
* @return An error as a string if any
154+
*/
155+
std::expected<void, std::string>
156+
saveToFile(std::string const& path) const;
157+
158+
std::expected<void, std::string>
159+
loadFromFile(std::string const& path, uint32_t minLatestSequence) override;
141160
};
142161

143162
} // namespace data

src/data/LedgerCacheInterface.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727

2828
#include <cstddef>
2929
#include <cstdint>
30+
#include <expected>
3031
#include <optional>
32+
#include <string>
3133
#include <vector>
3234

3335
namespace data {
@@ -168,6 +170,17 @@ class LedgerCacheInterface {
168170
*/
169171
virtual void
170172
waitUntilCacheContainsSeq(uint32_t seq) = 0;
173+
174+
/**
175+
* @brief Load the cache from file
176+
* @note This operation takes about 7 seconds and it keeps mtx_ exclusively locked
177+
*
178+
* @param path The file path to load data from
179+
* @param minLatestSequence The minimum allowed value of the latestLedgerSequence in cache file
180+
* @return An error as a string if any
181+
*/
182+
[[nodiscard]] virtual std::expected<void, std::string>
183+
loadFromFile(std::string const& path, uint32_t minLatestSequence) = 0;
171184
};
172185

173186
} // namespace data

src/data/Types.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ struct MPTHoldersAndCursor {
247247
struct LedgerRange {
248248
std::uint32_t minSequence = 0;
249249
std::uint32_t maxSequence = 0;
250+
251+
bool
252+
operator==(LedgerRange const&) const = default;
250253
};
251254

252255
/**

0 commit comments

Comments
 (0)