feat: Cache FetchLedgerBySeq (#2014)

fixes #1758
This commit is contained in:
Peter Chen
2025-05-12 11:45:54 -04:00
committed by GitHub
parent aa910ba889
commit 0b0794d9bf
8 changed files with 330 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ target_sources(
BackendCounters.cpp
BackendInterface.cpp
LedgerCache.cpp
LedgerHeaderCache.cpp
cassandra/impl/Future.cpp
cassandra/impl/Cluster.cpp
cassandra/impl/Batch.cpp

View File

@@ -22,6 +22,7 @@
#include "data/BackendInterface.hpp"
#include "data/DBHelpers.hpp"
#include "data/LedgerCacheInterface.hpp"
#include "data/LedgerHeaderCache.hpp"
#include "data/Types.hpp"
#include "data/cassandra/Concepts.hpp"
#include "data/cassandra/Handle.hpp"
@@ -62,6 +63,8 @@
#include <utility>
#include <vector>
class CacheBackendCassandraTest;
namespace data::cassandra {
/**
@@ -71,21 +74,27 @@ namespace data::cassandra {
*
* @tparam SettingsProviderType The settings provider type to use
* @tparam ExecutionStrategyType The execution strategy type to use
* @tparam FetchLedgerCacheType The ledger header cache type to use
*/
template <SomeSettingsProvider SettingsProviderType, SomeExecutionStrategy ExecutionStrategyType>
template <
SomeSettingsProvider SettingsProviderType,
SomeExecutionStrategy ExecutionStrategyType,
typename FetchLedgerCacheType = FetchLedgerCache>
class BasicCassandraBackend : public BackendInterface {
util::Logger log_{"Backend"};
SettingsProviderType settingsProvider_;
Schema<SettingsProviderType> schema_;
std::atomic_uint32_t ledgerSequence_ = 0u;
friend class ::CacheBackendCassandraTest;
protected:
Handle handle_;
// have to be mutable because BackendInterface constness :(
mutable ExecutionStrategyType executor_;
// TODO: move to interface level
mutable FetchLedgerCacheType ledgerCache_{};
public:
/**
@@ -129,7 +138,6 @@ public:
LOG(log_.error()) << error;
throw std::runtime_error(error);
}
LOG(log_.info()) << "Created (revamped) CassandraBackend";
}
@@ -263,11 +271,16 @@ public:
std::optional<ripple::LedgerHeader>
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context yield) const override
{
if (auto const lock = ledgerCache_.get(); lock.has_value() && lock->seq == sequence)
return lock->ledger;
auto const res = executor_.read(yield, schema_->selectLedgerBySeq, sequence);
if (res) {
if (auto const& result = res.value(); result) {
if (auto const maybeValue = result.template get<std::vector<unsigned char>>(); maybeValue) {
return util::deserializeHeader(ripple::makeSlice(*maybeValue));
auto const header = util::deserializeHeader(ripple::makeSlice(*maybeValue));
ledgerCache_.put(FetchLedgerCache::CacheEntry{header, sequence});
return header;
}
LOG(log_.error()) << "Could not fetch ledger by sequence - no rows";

View File

@@ -0,0 +1,46 @@
//------------------------------------------------------------------------------
/*
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 "data/LedgerHeaderCache.hpp"
#include "util/Mutex.hpp"
#include <mutex>
#include <optional>
#include <shared_mutex>
namespace data {
FetchLedgerCache::FetchLedgerCache() = default;
void
FetchLedgerCache::put(CacheEntry const& cacheEntry)
{
auto lock = mutex_.lock<std::unique_lock>();
*lock = cacheEntry;
}
std::optional<FetchLedgerCache::CacheEntry>
FetchLedgerCache::get() const
{
auto const lock = mutex_.lock<std::shared_lock>();
return lock.get();
}
} // namespace data

View File

@@ -0,0 +1,85 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#pragma once
#include "util/Mutex.hpp"
#include <xrpl/protocol/LedgerHeader.h>
#include <cstdint>
#include <optional>
#include <shared_mutex>
namespace data {
/**
* @brief A simple cache holding one `ripple::LedgerHeader` to reduce DB lookups.
*
* Used internally by backend implementations. When a ledger header is
* fetched via `FetchLedgerBySeq` (often triggered by RPC commands),
* the result can be stored here. Subsequent requests for the same ledger
* sequence can proceed to retrieve the header from this cache, avoiding unnecessary
* database reads and improving performance.
*/
class FetchLedgerCache {
public:
FetchLedgerCache();
/**
* @brief Struct to store ledger header cache entry and the sequence it belongs to
*/
struct CacheEntry {
ripple::LedgerHeader ledger;
uint32_t seq{};
/**
* @brief Comparing CacheEntry. Used in testing for EXPECT_CALL
*
* @param other The other cacheEntry to compare
* @return true if two CacheEntry is the same, false otherwise
*/
bool
operator==(CacheEntry const& other) const
{
return ledger.hash == other.ledger.hash && seq == other.seq;
}
};
/**
* @brief Put CacheEntry into thread-safe container
*
* @param cacheEntry The Cache to store into thread-safe container.
*/
void
put(CacheEntry const& cacheEntry);
/**
* @brief Read CacheEntry from thread-safe container.
*
* @return Optional CacheEntry, depending on if it exists in thread-safe container or not.
*/
std::optional<CacheEntry>
get() const;
private:
mutable util::Mutex<std::optional<CacheEntry>, std::shared_mutex> mutex_;
};
} // namespace data

View File

@@ -0,0 +1,35 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#pragma once
#include "data/LedgerHeaderCache.hpp"
#include <gmock/gmock.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <optional>
struct MockLedgerHeaderCache {
MockLedgerHeaderCache() = default;
using CacheEntry = data::FetchLedgerCache::CacheEntry;
MOCK_METHOD(void, put, (CacheEntry), ());
MOCK_METHOD(std::optional<CacheEntry>, get, (), (const));
};

View File

@@ -21,13 +21,16 @@
#include "data/CassandraBackend.hpp"
#include "data/DBHelpers.hpp"
#include "data/LedgerCache.hpp"
#include "data/LedgerHeaderCache.hpp"
#include "data/Types.hpp"
#include "data/cassandra/Handle.hpp"
#include "data/cassandra/SettingsProvider.hpp"
#include "data/cassandra/Types.hpp"
#include "etl/NFTHelpers.hpp"
#include "rpc/RPCHelpers.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/LedgerUtils.hpp"
#include "util/MockLedgerHeaderCache.hpp"
#include "util/MockPrometheus.hpp"
#include "util/Random.hpp"
#include "util/StringUtils.hpp"
@@ -42,6 +45,7 @@
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_hash.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
@@ -78,7 +82,7 @@ using namespace prometheus;
using namespace data::cassandra;
class BackendCassandraTest : public SyncAsioContextTest, public WithPrometheus {
class BackendCassandraTestBase : public SyncAsioContextTest, public WithPrometheus {
protected:
ClioConfigDefinition cfg_{
{"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra")},
@@ -106,17 +110,23 @@ protected:
{"read_only", ConfigValue{ConfigType::Boolean}.defaultValue(false)}
};
static constexpr auto kRAWHEADER =
"03C3141A01633CD656F91B4EBB5EB89B791BD34DBC8A04BB6F407C5335BC54351E"
"DD733898497E809E04074D14D271E4832D7888754F9230800761563A292FA2315A"
"6DB6FE30CC5909B285080FCD6773CC883F9FE0EE4D439340AC592AADB973ED3CF5"
"3E2232B33EF57CECAC2816E3122816E31A0A00F8377CD95DFA484CFAE282656A58"
"CE5AA29652EFFD80AC59CD91416E4E13DBBE";
ObjectView obj_ = cfg_.getObject("database.cassandra");
SettingsProvider settingsProvider_{obj_};
// recreated for each test
data::LedgerCache cache_;
std::unique_ptr<BackendInterface> backend_{std::make_unique<CassandraBackend>(settingsProvider_, cache_, false)};
std::default_random_engine randomEngine_{0};
public:
~BackendCassandraTest() override
~BackendCassandraTestBase() override
{
// drop the keyspace for next test
Handle const handle{TestGlobals::instance().backendHost};
@@ -125,6 +135,11 @@ public:
}
};
class BackendCassandraTest : public BackendCassandraTestBase {
protected:
std::unique_ptr<BackendInterface> backend_{std::make_unique<CassandraBackend>(settingsProvider_, cache_, false)};
};
TEST_F(BackendCassandraTest, Basic)
{
std::atomic_bool done = false;
@@ -898,12 +913,6 @@ TEST_F(BackendCassandraTest, CacheIntegration)
boost::asio::spawn(ctx_, [this, &done, &work](boost::asio::yield_context yield) {
backend_->cache().setFull();
std::string const rawHeader =
"03C3141A01633CD656F91B4EBB5EB89B791BD34DBC8A04BB6F407C5335BC54351E"
"DD733898497E809E04074D14D271E4832D7888754F9230800761563A292FA2315A"
"6DB6FE30CC5909B285080FCD6773CC883F9FE0EE4D439340AC592AADB973ED3CF5"
"3E2232B33EF57CECAC2816E3122816E31A0A00F8377CD95DFA484CFAE282656A58"
"CE5AA29652EFFD80AC59CD91416E4E13DBBE";
// this account is not related to the above transaction and
// metadata
std::string const accountHex =
@@ -912,7 +921,7 @@ TEST_F(BackendCassandraTest, CacheIntegration)
"142252F328CF91263417762570D67220CCB33B1370";
std::string const accountIndexHex = "E0311EB450B6177F969B94DBDDA83E99B7A0576ACD9079573876F16C0C004F06";
std::string rawHeaderBlob = hexStringToBinaryString(rawHeader);
std::string rawHeaderBlob = hexStringToBinaryString(kRAWHEADER);
std::string accountBlob = hexStringToBinaryString(accountHex);
std::string const accountIndexBlob = hexStringToBinaryString(accountIndexHex);
ripple::LedgerHeader const lgrInfo = util::deserializeHeader(ripple::makeSlice(rawHeaderBlob));
@@ -1294,8 +1303,63 @@ TEST_F(BackendCassandraTest, CacheIntegration)
ASSERT_EQ(done, true);
}
class CacheBackendCassandraTest : public BackendCassandraTestBase {
protected:
using TestBackendType = data::cassandra::BasicCassandraBackend<
SettingsProvider,
data::cassandra::impl::DefaultExecutionStrategy<>,
MockLedgerHeaderCache>;
std::unique_ptr<BackendInterface> backend_{std::make_unique<TestBackendType>(settingsProvider_, cache_, false)};
public:
MockLedgerHeaderCache&
getMockCache()
{
return dynamic_cast<TestBackendType&>(*backend_).ledgerCache_;
}
};
TEST_F(CacheBackendCassandraTest, CacheFetchLedgerBySeq)
{
runSpawn([&](boost::asio::yield_context yield) {
auto rawHeaderBlob = hexStringToBinaryString(kRAWHEADER);
ripple::LedgerHeader lgrInfo = util::deserializeHeader(ripple::makeSlice(rawHeaderBlob));
backend_->writeLedger(lgrInfo, std::move(rawHeaderBlob));
auto const testLedgerSeq = lgrInfo.seq;
ASSERT_TRUE(backend_->finishWrites(lgrInfo.seq));
EXPECT_CALL(getMockCache(), put(data::FetchLedgerCache::CacheEntry{lgrInfo, testLedgerSeq}));
{
testing::InSequence s;
// first time, getSeq doesn't match ledger sequence
EXPECT_CALL(getMockCache(), get()).WillOnce(testing::Return(std::nullopt));
// second time, it would be cached
EXPECT_CALL(getMockCache(), get())
.WillOnce(testing::Return(data::FetchLedgerCache::CacheEntry{.ledger = lgrInfo, .seq = testLedgerSeq}));
}
{
// backend should cache the result of fetchLedgerBySequence
auto const ledger = backend_->fetchLedgerBySequence(testLedgerSeq, yield);
ASSERT_TRUE(ledger.has_value());
EXPECT_EQ(ledger->seq, lgrInfo.seq);
}
{
// Second call: should return from cache
auto const ledger = backend_->fetchLedgerBySequence(testLedgerSeq, yield);
ASSERT_TRUE(ledger.has_value());
EXPECT_EQ(ledger->seq, lgrInfo.seq);
}
});
}
struct BackendCassandraNodeMessageTest : BackendCassandraTest {
boost::uuids::random_generator generateUuid{};
boost::uuids::random_generator generateUuid;
};
TEST_F(BackendCassandraNodeMessageTest, UpdateFetch)

View File

@@ -14,6 +14,7 @@ target_sources(
data/LedgerCacheTests.cpp
data/cassandra/AsyncExecutorTests.cpp
data/cassandra/ExecutionStrategyTests.cpp
data/cassandra/LedgerHeaderCacheTests.cpp
data/cassandra/RetryPolicyTests.cpp
data/cassandra/SettingsProviderTests.cpp
# Cluster

View File

@@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
/*
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 "data/LedgerHeaderCache.hpp"
#include "util/TestObject.hpp"
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/LedgerHeader.h>
using namespace data;
using Test = ::testing::Test;
constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constinit auto const kLEDGER_HASH2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
class FetchLedgerCacheTest : public Test {
protected:
FetchLedgerCache cache_;
};
TEST_F(FetchLedgerCacheTest, DefaultCacheIsEmpty)
{
auto const result = cache_.get();
EXPECT_FALSE(result.has_value());
}
TEST_F(FetchLedgerCacheTest, CanStoreAndRetrieveEntry)
{
auto const ledger = createLedgerHeader(kLEDGER_HASH, 42);
FetchLedgerCache::CacheEntry entry{.ledger = ledger, .seq = 42};
cache_.put(entry);
auto const result = cache_.get();
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), entry);
}
TEST_F(FetchLedgerCacheTest, PutOverwritesPreviousEntry)
{
auto const ledger1 = createLedgerHeader(kLEDGER_HASH, 1);
auto const ledger2 = createLedgerHeader(kLEDGER_HASH2, 2);
FetchLedgerCache::CacheEntry entry1{.ledger = ledger1, .seq = 1};
FetchLedgerCache::CacheEntry entry2{.ledger = ledger2, .seq = 2};
cache_.put(entry1);
cache_.put(entry2);
auto const result = cache_.get();
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), entry2);
}