mirror of
https://github.com/XRPLF/clio.git
synced 2026-06-05 17:56:45 +00:00
fix: Start without cache file (#2976)
#2830 introduced a bug that clio couldn't start without having a cache file. This PR fixes the problem.
This commit is contained in:
@@ -126,7 +126,7 @@ ClioApplication::run(bool const useNgWebServer)
|
||||
auto systemState = etl::SystemState::makeSystemState(config_);
|
||||
|
||||
cluster::ClusterCommunicationService clusterCommunicationService{
|
||||
backend, std::make_unique<etl::WriterState>(systemState)
|
||||
backend, std::make_unique<etl::WriterState>(systemState, cache)
|
||||
};
|
||||
clusterCommunicationService.run();
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace {
|
||||
struct JsonFields {
|
||||
static constexpr std::string_view const kUPDATE_TIME = "update_time";
|
||||
static constexpr std::string_view const kDB_ROLE = "db_role";
|
||||
static constexpr std::string_view const kETL_STARTED = "etl_started";
|
||||
static constexpr std::string_view const kCACHE_IS_FULL = "cache_is_full";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -56,14 +58,15 @@ ClioNode::from(ClioNode::Uuid uuid, etl::WriterStateInterface const& writerState
|
||||
if (writerState.isFallback()) {
|
||||
return ClioNode::DbRole::Fallback;
|
||||
}
|
||||
if (writerState.isLoadingCache()) {
|
||||
return ClioNode::DbRole::LoadingCache;
|
||||
}
|
||||
|
||||
return writerState.isWriting() ? ClioNode::DbRole::Writer : ClioNode::DbRole::NotWriter;
|
||||
}();
|
||||
return ClioNode{
|
||||
.uuid = std::move(uuid), .updateTime = std::chrono::system_clock::now(), .dbRole = dbRole
|
||||
.uuid = std::move(uuid),
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = dbRole,
|
||||
.etlStarted = writerState.isEtlStarted(),
|
||||
.cacheIsFull = writerState.isCacheFull()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +75,9 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, ClioNode const&
|
||||
{
|
||||
jv = {
|
||||
{JsonFields::kUPDATE_TIME, util::systemTpToUtcStr(node.updateTime, ClioNode::kTIME_FORMAT)},
|
||||
{JsonFields::kDB_ROLE, static_cast<int64_t>(node.dbRole)}
|
||||
{JsonFields::kDB_ROLE, static_cast<int64_t>(node.dbRole)},
|
||||
{JsonFields::kETL_STARTED, node.etlStarted},
|
||||
{JsonFields::kCACHE_IS_FULL, node.cacheIsFull}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,12 +95,17 @@ tag_invoke(boost::json::value_to_tag<ClioNode>, boost::json::value const& jv)
|
||||
if (dbRoleValue > static_cast<int64_t>(ClioNode::DbRole::MAX))
|
||||
throw std::runtime_error("Invalid db_role value");
|
||||
|
||||
auto const etlStarted = jv.as_object().at(JsonFields::kETL_STARTED).as_bool();
|
||||
auto const cacheIsFull = jv.as_object().at(JsonFields::kCACHE_IS_FULL).as_bool();
|
||||
|
||||
return ClioNode{
|
||||
// Json data doesn't contain uuid so leaving it empty here. It will be filled outside of
|
||||
// this parsing
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(),
|
||||
.updateTime = updateTime.value(),
|
||||
.dbRole = static_cast<ClioNode::DbRole>(dbRoleValue)
|
||||
.dbRole = static_cast<ClioNode::DbRole>(dbRoleValue),
|
||||
.etlStarted = etlStarted,
|
||||
.cacheIsFull = cacheIsFull
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,22 +52,17 @@ struct ClioNode {
|
||||
* from the cluster communication mechanism to the slower but more reliable
|
||||
* database-based conflict detection mechanism.
|
||||
*/
|
||||
enum class DbRole {
|
||||
ReadOnly = 0,
|
||||
LoadingCache = 1,
|
||||
NotWriter = 2,
|
||||
Writer = 3,
|
||||
Fallback = 4,
|
||||
MAX = 4
|
||||
};
|
||||
enum class DbRole { ReadOnly = 0, NotWriter = 1, Writer = 2, Fallback = 3, MAX = 3 };
|
||||
|
||||
using Uuid = std::shared_ptr<boost::uuids::uuid>;
|
||||
using CUuid = std::shared_ptr<boost::uuids::uuid const>;
|
||||
|
||||
Uuid uuid; ///< The UUID of the node.
|
||||
std::chrono::system_clock::time_point
|
||||
updateTime; ///< The time the data about the node was last updated.
|
||||
DbRole dbRole; ///< The database role of the node
|
||||
updateTime; ///< The time the data about the node was last updated.
|
||||
DbRole dbRole; ///< The database role of the node
|
||||
bool etlStarted; ///< Whether the ETL monitor has started on this node
|
||||
bool cacheIsFull; ///< Whether the ledger cache is fully loaded on this node
|
||||
|
||||
/**
|
||||
* @brief Create a ClioNode from writer state.
|
||||
|
||||
@@ -84,21 +84,36 @@ WriterDecider::onNewState(
|
||||
return *lhs.uuid < *rhs.uuid;
|
||||
});
|
||||
|
||||
auto const it = std::ranges::find_if(clusterData, [](ClioNode const& node) {
|
||||
return node.dbRole == ClioNode::DbRole::NotWriter or
|
||||
node.dbRole == ClioNode::DbRole::Writer;
|
||||
auto it = std::ranges::find_if(clusterData, [](ClioNode const& node) {
|
||||
return node.etlStarted and node.cacheIsFull and
|
||||
(node.dbRole == ClioNode::DbRole::NotWriter or
|
||||
node.dbRole == ClioNode::DbRole::Writer);
|
||||
});
|
||||
|
||||
if (it == clusterData.end()) {
|
||||
// No writer nodes in the cluster yet
|
||||
auto electNode = [&selfId, &writerState](auto it) {
|
||||
if (*it->uuid == *selfId) {
|
||||
writerState->startWriting();
|
||||
} else {
|
||||
writerState->giveUpWriting();
|
||||
}
|
||||
};
|
||||
if (it != clusterData.end()) {
|
||||
electNode(it);
|
||||
return;
|
||||
}
|
||||
|
||||
if (*it->uuid == *selfId) {
|
||||
writerState->startWriting();
|
||||
} else {
|
||||
writerState->giveUpWriting();
|
||||
// Try to find a node with at least started ETL
|
||||
it = std::ranges::find_if(clusterData, [](ClioNode const& node) {
|
||||
return node.etlStarted and
|
||||
(node.dbRole == ClioNode::DbRole::NotWriter or
|
||||
node.dbRole == ClioNode::DbRole::Writer);
|
||||
});
|
||||
|
||||
if (it != clusterData.end()) {
|
||||
electNode(it);
|
||||
return;
|
||||
}
|
||||
writerState->giveUpWriting();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ public:
|
||||
}
|
||||
|
||||
if (loadCacheFromFile()) {
|
||||
// Cache file may contain outdated data, so fetch whatever left up to seq from DB
|
||||
updateCacheToSeq(seq);
|
||||
cache_.get().setFull();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,9 +194,23 @@ private:
|
||||
|
||||
LOG(log_.info()) << "Loaded cache from file in " << duration_ms
|
||||
<< " ms. Latest sequence: " << cache_.get().latestLedgerSequence();
|
||||
backend_->forceUpdateRange(cache_.get().latestLedgerSequence());
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
updateCacheToSeq(uint32_t const seq)
|
||||
{
|
||||
while (cache_.get().latestLedgerSequence() < seq) {
|
||||
auto const seqToLoad = cache_.get().latestLedgerSequence() + 1;
|
||||
LOG(log_.info()) << "Fetching ledger " << seqToLoad
|
||||
<< "from DB after loading cache from file";
|
||||
auto const diff = data::synchronousAndRetryOnTimeout([this, seqToLoad](auto yield) {
|
||||
return backend_->fetchLedgerDiff(seqToLoad, yield);
|
||||
});
|
||||
cache_.get().update(diff, seqToLoad);
|
||||
LOG(log_.info()) << "Updated cache to " << seqToLoad;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace etl
|
||||
|
||||
@@ -215,13 +215,13 @@ ETLService::run()
|
||||
return;
|
||||
}
|
||||
|
||||
auto const nextSequence = syncCacheWithDb();
|
||||
auto const nextSequence = rng->maxSequence + 1;
|
||||
LOG(log_.debug()) << "Database is populated. Starting monitor loop. sequence = "
|
||||
<< nextSequence;
|
||||
|
||||
startMonitor(nextSequence);
|
||||
|
||||
state_->isLoadingCache = false;
|
||||
state_->etlStarted = true;
|
||||
|
||||
// If we are a writer as the result of loading the initial ledger - start loading
|
||||
if (state_->isWriting)
|
||||
@@ -356,24 +356,6 @@ ETLService::loadInitialLedgerIfNeeded()
|
||||
return rng;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
ETLService::syncCacheWithDb()
|
||||
{
|
||||
auto rng = backend_->hardFetchLedgerRangeNoThrow();
|
||||
|
||||
while (not backend_->cache().isDisabled() and
|
||||
rng->maxSequence > backend_->cache().latestLedgerSequence()) {
|
||||
LOG(log_.info()) << "Syncing cache with DB. DB latest seq: " << rng->maxSequence
|
||||
<< ". Cache latest seq: " << backend_->cache().latestLedgerSequence();
|
||||
for (auto seq = backend_->cache().latestLedgerSequence(); seq <= rng->maxSequence; ++seq) {
|
||||
LOG(log_.info()) << "ETLService (via syncCacheWithDb) got new seq from db: " << seq;
|
||||
updateCache(seq);
|
||||
}
|
||||
rng = backend_->hardFetchLedgerRangeNoThrow();
|
||||
}
|
||||
return rng->maxSequence + 1;
|
||||
}
|
||||
|
||||
void
|
||||
ETLService::updateCache(uint32_t seq)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace etl {
|
||||
@@ -36,11 +37,6 @@ namespace etl {
|
||||
* @brief Represents the state of the ETL subsystem.
|
||||
*/
|
||||
struct SystemState {
|
||||
SystemState()
|
||||
{
|
||||
isLoadingCache = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Factory method to create a SystemState instance.
|
||||
*
|
||||
@@ -74,12 +70,8 @@ struct SystemState {
|
||||
"Whether the process is writing to the database"
|
||||
);
|
||||
|
||||
/** @brief Whether the process is still loading cache after startup. */
|
||||
util::prometheus::Bool isLoadingCache = PrometheusService::boolMetric(
|
||||
"etl_loading_cache",
|
||||
util::prometheus::Labels{},
|
||||
"Whether etl is loading cache after clio startup"
|
||||
);
|
||||
/** @brief Shows whether ETL started monitor and ready to become a writer if needed */
|
||||
std::atomic_bool etlStarted{false};
|
||||
|
||||
/**
|
||||
* @brief Commands for controlling the ETL writer state.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "etl/WriterState.hpp"
|
||||
|
||||
#include "data/LedgerCacheInterface.hpp"
|
||||
#include "etl/SystemState.hpp"
|
||||
|
||||
#include <memory>
|
||||
@@ -26,7 +27,11 @@
|
||||
|
||||
namespace etl {
|
||||
|
||||
WriterState::WriterState(std::shared_ptr<SystemState> state) : systemState_(std::move(state))
|
||||
WriterState::WriterState(
|
||||
std::shared_ptr<SystemState> state,
|
||||
data::LedgerCacheInterface const& cache
|
||||
)
|
||||
: systemState_(std::move(state)), cache_(cache)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -73,9 +78,15 @@ WriterState::isFallback() const
|
||||
}
|
||||
|
||||
bool
|
||||
WriterState::isLoadingCache() const
|
||||
WriterState::isEtlStarted() const
|
||||
{
|
||||
return systemState_->isLoadingCache;
|
||||
return systemState_->etlStarted;
|
||||
}
|
||||
|
||||
bool
|
||||
WriterState::isCacheFull() const
|
||||
{
|
||||
return cache_.get().isFull();
|
||||
}
|
||||
|
||||
std::unique_ptr<WriterStateInterface>
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/LedgerCacheInterface.hpp"
|
||||
#include "etl/SystemState.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace etl {
|
||||
@@ -88,12 +90,20 @@ public:
|
||||
setWriterDecidingFallback() = 0;
|
||||
|
||||
/**
|
||||
* @brief Whether clio is still loading cache after startup.
|
||||
* @brief Whether the ETL monitor has started and the node is ready to become a writer.
|
||||
*
|
||||
* @return true if clio is still loading cache, false otherwise.
|
||||
* @return true if ETL has started the monitor loop, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
isLoadingCache() const = 0;
|
||||
isEtlStarted() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Whether the ledger cache is fully loaded.
|
||||
*
|
||||
* @return true if the cache is full, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
isCacheFull() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Create a clone of this writer state.
|
||||
@@ -119,13 +129,16 @@ class WriterState : public WriterStateInterface {
|
||||
private:
|
||||
std::shared_ptr<SystemState>
|
||||
systemState_; /**< @brief Shared system state for ETL coordination */
|
||||
std::reference_wrapper<data::LedgerCacheInterface const> cache_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a WriterState with the given system state.
|
||||
* @brief Construct a WriterState with the given system state and cache.
|
||||
*
|
||||
* @param state Shared pointer to the system state for coordination
|
||||
* @param cache The ledger cache used to report cache fullness
|
||||
*/
|
||||
WriterState(std::shared_ptr<SystemState> state);
|
||||
WriterState(std::shared_ptr<SystemState> state, data::LedgerCacheInterface const& cache);
|
||||
|
||||
bool
|
||||
isReadOnly() const override;
|
||||
@@ -172,13 +185,13 @@ public:
|
||||
bool
|
||||
isFallback() const override;
|
||||
|
||||
/**
|
||||
* @brief Whether clio is still loading cache after startup.
|
||||
*
|
||||
* @return true if clio is still loading cache, false otherwise.
|
||||
*/
|
||||
/** @copydoc WriterStateInterface::isEtlStarted */
|
||||
bool
|
||||
isLoadingCache() const override;
|
||||
isEtlStarted() const override;
|
||||
|
||||
/** @copydoc WriterStateInterface::isCacheFull */
|
||||
bool
|
||||
isCacheFull() const override;
|
||||
|
||||
/**
|
||||
* @brief Create a clone of this writer state.
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
struct MockLedgerCache : data::LedgerCacheInterface {
|
||||
MOCK_METHOD(
|
||||
void,
|
||||
updateImp,
|
||||
updateImpl,
|
||||
(std::vector<data::LedgerObject> const& a, uint32_t b, bool c),
|
||||
()
|
||||
);
|
||||
@@ -43,7 +43,7 @@ struct MockLedgerCache : data::LedgerCacheInterface {
|
||||
void
|
||||
update(std::vector<data::LedgerObject> const& a, uint32_t b, bool c = false) override
|
||||
{
|
||||
updateImp(a, b, c);
|
||||
updateImpl(a, b, c);
|
||||
}
|
||||
|
||||
MOCK_METHOD(
|
||||
|
||||
@@ -32,7 +32,8 @@ struct MockWriterStateBase : public etl::WriterStateInterface {
|
||||
MOCK_METHOD(void, giveUpWriting, (), (override));
|
||||
MOCK_METHOD(void, setWriterDecidingFallback, (), (override));
|
||||
MOCK_METHOD(bool, isFallback, (), (const, override));
|
||||
MOCK_METHOD(bool, isLoadingCache, (), (const, override));
|
||||
MOCK_METHOD(bool, isEtlStarted, (), (const, override));
|
||||
MOCK_METHOD(bool, isCacheFull, (), (const, override));
|
||||
MOCK_METHOD(std::unique_ptr<etl::WriterStateInterface>, clone, (), (const, override));
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,12 @@ TEST_F(ClusterBackendTest, SubscribeToNewState)
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(callbackMock, Call)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly([this](
|
||||
@@ -127,6 +133,12 @@ TEST_F(ClusterBackendTest, Stop)
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
|
||||
clusterBackend.run();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
||||
@@ -156,6 +168,12 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataThrowsException)
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(callbackMock, Call)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(
|
||||
@@ -184,8 +202,10 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsDataWithOtherNodes)
|
||||
|
||||
auto const otherUuid = boost::uuids::random_generator{}();
|
||||
auto const otherNodeJson = R"JSON({
|
||||
"db_role": 3,
|
||||
"update_time": "2025-01-15T10:30:00Z"
|
||||
"db_role": 2,
|
||||
"update_time": "2025-01-15T10:30:00Z",
|
||||
"etl_started": false,
|
||||
"cache_is_full": false
|
||||
})JSON";
|
||||
|
||||
EXPECT_CALL(*backend_, fetchClioNodesData)
|
||||
@@ -206,7 +226,10 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsDataWithOtherNodes)
|
||||
EXPECT_CALL(writerStateRef, isFallback)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isLoadingCache)
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isWriting)
|
||||
@@ -257,7 +280,9 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsOnlySelfData)
|
||||
|
||||
auto const selfNodeJson = R"JSON({
|
||||
"db_role": 1,
|
||||
"update_time": "2025-01-16T10:30:00Z"
|
||||
"update_time": "2025-01-16T10:30:00Z",
|
||||
"etl_started": false,
|
||||
"cache_is_full": false
|
||||
})JSON";
|
||||
|
||||
EXPECT_CALL(*backend_, fetchClioNodesData).Times(testing::AtLeast(1)).WillRepeatedly([&]() {
|
||||
@@ -271,6 +296,12 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsOnlySelfData)
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(callbackMock, Call)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly([this](
|
||||
@@ -320,6 +351,12 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsInvalidJson)
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(callbackMock, Call)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly([this, invalidJson](
|
||||
@@ -368,6 +405,12 @@ TEST_F(ClusterBackendTest, FetchClioNodesDataReturnsValidJsonButCannotConvertToC
|
||||
EXPECT_CALL(writerStateRef, isReadOnly)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(callbackMock, Call)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(
|
||||
@@ -406,7 +449,10 @@ TEST_F(ClusterBackendTest, WriteNodeMessageWritesSelfDataWithRecentTimestampAndD
|
||||
EXPECT_CALL(writerStateRef, isFallback)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isLoadingCache)
|
||||
EXPECT_CALL(writerStateRef, isEtlStarted)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isCacheFull)
|
||||
.Times(testing::AtLeast(1))
|
||||
.WillRepeatedly(testing::Return(false));
|
||||
EXPECT_CALL(writerStateRef, isWriting)
|
||||
|
||||
@@ -51,7 +51,9 @@ TEST_F(ClioNodeTest, Serialization)
|
||||
ClioNode const node{
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(boost::uuids::random_generator()()),
|
||||
.updateTime = updateTime,
|
||||
.dbRole = ClioNode::DbRole::Writer
|
||||
.dbRole = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
|
||||
boost::json::value jsonValue;
|
||||
@@ -66,23 +68,38 @@ TEST_F(ClioNodeTest, Serialization)
|
||||
EXPECT_TRUE(obj.contains("db_role"));
|
||||
EXPECT_TRUE(obj.at("db_role").is_number());
|
||||
EXPECT_EQ(obj.at("db_role").as_int64(), static_cast<int64_t>(node.dbRole));
|
||||
|
||||
EXPECT_TRUE(obj.contains("etl_started"));
|
||||
EXPECT_EQ(obj.at("etl_started").as_bool(), node.etlStarted);
|
||||
|
||||
EXPECT_TRUE(obj.contains("cache_is_full"));
|
||||
EXPECT_EQ(obj.at("cache_is_full").as_bool(), node.cacheIsFull);
|
||||
}
|
||||
|
||||
TEST_F(ClioNodeTest, Deserialization)
|
||||
{
|
||||
boost::json::value const jsonValue = {{"update_time", updateTimeStr}, {"db_role", 1}};
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr},
|
||||
{"db_role", 1},
|
||||
{"etl_started", true},
|
||||
{"cache_is_full", false}
|
||||
};
|
||||
|
||||
ClioNode node{
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(),
|
||||
.updateTime = {},
|
||||
.dbRole = ClioNode::DbRole::ReadOnly
|
||||
.dbRole = ClioNode::DbRole::ReadOnly,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
ASSERT_NO_THROW(node = boost::json::value_to<ClioNode>(jsonValue));
|
||||
|
||||
EXPECT_NE(node.uuid, nullptr);
|
||||
EXPECT_EQ(*node.uuid, boost::uuids::uuid{});
|
||||
EXPECT_EQ(node.updateTime, updateTime);
|
||||
EXPECT_EQ(node.dbRole, ClioNode::DbRole::LoadingCache);
|
||||
EXPECT_EQ(node.dbRole, ClioNode::DbRole::NotWriter);
|
||||
EXPECT_TRUE(node.etlStarted);
|
||||
EXPECT_FALSE(node.cacheIsFull);
|
||||
}
|
||||
|
||||
TEST_F(ClioNodeTest, DeserializationInvalidTime)
|
||||
@@ -100,6 +117,22 @@ TEST_F(ClioNodeTest, DeserializationMissingTime)
|
||||
EXPECT_THROW(boost::json::value_to<ClioNode>(jsonValue), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(ClioNodeTest, DeserializationMissingEtlStarted)
|
||||
{
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr}, {"db_role", 1}, {"cache_is_full", false}
|
||||
};
|
||||
EXPECT_THROW(boost::json::value_to<ClioNode>(jsonValue), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(ClioNodeTest, DeserializationMissingCacheIsFull)
|
||||
{
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr}, {"db_role", 1}, {"etl_started", true}
|
||||
};
|
||||
EXPECT_THROW(boost::json::value_to<ClioNode>(jsonValue), std::runtime_error);
|
||||
}
|
||||
|
||||
struct ClioNodeDbRoleTestBundle {
|
||||
std::string testName;
|
||||
ClioNode::DbRole role;
|
||||
@@ -112,10 +145,6 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
ClioNodeDbRoleTest,
|
||||
testing::Values(
|
||||
ClioNodeDbRoleTestBundle{.testName = "ReadOnly", .role = ClioNode::DbRole::ReadOnly},
|
||||
ClioNodeDbRoleTestBundle{
|
||||
.testName = "LoadingCache",
|
||||
.role = ClioNode::DbRole::LoadingCache
|
||||
},
|
||||
ClioNodeDbRoleTestBundle{.testName = "NotWriter", .role = ClioNode::DbRole::NotWriter},
|
||||
ClioNodeDbRoleTestBundle{.testName = "Writer", .role = ClioNode::DbRole::Writer},
|
||||
ClioNodeDbRoleTestBundle{.testName = "Fallback", .role = ClioNode::DbRole::Fallback}
|
||||
@@ -129,7 +158,9 @@ TEST_P(ClioNodeDbRoleTest, Serialization)
|
||||
ClioNode const node{
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(boost::uuids::random_generator()()),
|
||||
.updateTime = updateTime,
|
||||
.dbRole = param.role
|
||||
.dbRole = param.role,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
auto const jsonValue = boost::json::value_from(node);
|
||||
EXPECT_EQ(jsonValue.as_object().at("db_role").as_int64(), static_cast<int64_t>(param.role));
|
||||
@@ -139,7 +170,10 @@ TEST_P(ClioNodeDbRoleTest, Deserialization)
|
||||
{
|
||||
auto const param = GetParam();
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr}, {"db_role", static_cast<int64_t>(param.role)}
|
||||
{"update_time", updateTimeStr},
|
||||
{"db_role", static_cast<int64_t>(param.role)},
|
||||
{"etl_started", false},
|
||||
{"cache_is_full", false}
|
||||
};
|
||||
auto const node = boost::json::value_to<ClioNode>(jsonValue);
|
||||
EXPECT_EQ(node.dbRole, param.role);
|
||||
@@ -147,13 +181,20 @@ TEST_P(ClioNodeDbRoleTest, Deserialization)
|
||||
|
||||
TEST_F(ClioNodeDbRoleTest, DeserializationInvalidDbRole)
|
||||
{
|
||||
boost::json::value const jsonValue = {{"update_time", updateTimeStr}, {"db_role", 10}};
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr},
|
||||
{"db_role", 10},
|
||||
{"etl_started", false},
|
||||
{"cache_is_full", false}
|
||||
};
|
||||
EXPECT_THROW(boost::json::value_to<ClioNode>(jsonValue), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(ClioNodeDbRoleTest, DeserializationMissingDbRole)
|
||||
{
|
||||
boost::json::value const jsonValue = {{"update_time", updateTimeStr}};
|
||||
boost::json::value const jsonValue = {
|
||||
{"update_time", updateTimeStr}, {"etl_started", false}, {"cache_is_full", false}
|
||||
};
|
||||
EXPECT_THROW(boost::json::value_to<ClioNode>(jsonValue), std::runtime_error);
|
||||
}
|
||||
|
||||
@@ -161,8 +202,9 @@ struct ClioNodeFromTestBundle {
|
||||
std::string testName;
|
||||
bool readOnly;
|
||||
bool fallback;
|
||||
bool loadingCache;
|
||||
bool writing;
|
||||
bool etlStarted;
|
||||
bool cacheIsFull;
|
||||
ClioNode::DbRole expectedRole;
|
||||
};
|
||||
|
||||
@@ -181,40 +223,36 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
.testName = "ReadOnly",
|
||||
.readOnly = true,
|
||||
.fallback = false,
|
||||
.loadingCache = false,
|
||||
.writing = false,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false,
|
||||
.expectedRole = ClioNode::DbRole::ReadOnly
|
||||
},
|
||||
ClioNodeFromTestBundle{
|
||||
.testName = "Fallback",
|
||||
.readOnly = false,
|
||||
.fallback = true,
|
||||
.loadingCache = false,
|
||||
.writing = false,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false,
|
||||
.expectedRole = ClioNode::DbRole::Fallback
|
||||
},
|
||||
ClioNodeFromTestBundle{
|
||||
.testName = "LoadingCache",
|
||||
.testName = "NotWriter",
|
||||
.readOnly = false,
|
||||
.fallback = false,
|
||||
.loadingCache = true,
|
||||
.writing = false,
|
||||
.expectedRole = ClioNode::DbRole::LoadingCache
|
||||
},
|
||||
ClioNodeFromTestBundle{
|
||||
.testName = "NotWriterNotReadOnly",
|
||||
.readOnly = false,
|
||||
.fallback = false,
|
||||
.loadingCache = false,
|
||||
.writing = false,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false,
|
||||
.expectedRole = ClioNode::DbRole::NotWriter
|
||||
},
|
||||
ClioNodeFromTestBundle{
|
||||
.testName = "Writer",
|
||||
.readOnly = false,
|
||||
.fallback = false,
|
||||
.loadingCache = false,
|
||||
.writing = true,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true,
|
||||
.expectedRole = ClioNode::DbRole::Writer
|
||||
}
|
||||
),
|
||||
@@ -229,13 +267,11 @@ TEST_P(ClioNodeFromTest, FromWriterState)
|
||||
if (not param.readOnly) {
|
||||
EXPECT_CALL(writerState, isFallback()).WillOnce(testing::Return(param.fallback));
|
||||
if (not param.fallback) {
|
||||
EXPECT_CALL(writerState, isLoadingCache())
|
||||
.WillOnce(testing::Return(param.loadingCache));
|
||||
if (not param.loadingCache) {
|
||||
EXPECT_CALL(writerState, isWriting()).WillOnce(testing::Return(param.writing));
|
||||
}
|
||||
EXPECT_CALL(writerState, isWriting()).WillOnce(testing::Return(param.writing));
|
||||
}
|
||||
}
|
||||
EXPECT_CALL(writerState, isEtlStarted()).WillOnce(testing::Return(param.etlStarted));
|
||||
EXPECT_CALL(writerState, isCacheFull()).WillOnce(testing::Return(param.cacheIsFull));
|
||||
|
||||
auto const beforeTime = std::chrono::system_clock::now();
|
||||
auto const node = ClioNode::from(uuid, writerState);
|
||||
@@ -243,6 +279,8 @@ TEST_P(ClioNodeFromTest, FromWriterState)
|
||||
|
||||
EXPECT_EQ(node.uuid, uuid);
|
||||
EXPECT_EQ(node.dbRole, param.expectedRole);
|
||||
EXPECT_EQ(node.etlStarted, param.etlStarted);
|
||||
EXPECT_EQ(node.cacheIsFull, param.cacheIsFull);
|
||||
EXPECT_GE(node.updateTime, beforeTime);
|
||||
EXPECT_LE(node.updateTime, afterTime);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ struct ClusterCommunicationServiceTest : util::prometheus::WithPrometheus, MockB
|
||||
return ClioNode{
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(uuid),
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = role
|
||||
.dbRole = role,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,17 +71,23 @@ TEST_F(MetricsTest, OnNewStateWithValidClusterData)
|
||||
ClioNode const node1{
|
||||
.uuid = uuid1,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::Writer
|
||||
.dbRole = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true
|
||||
};
|
||||
ClioNode const node2{
|
||||
.uuid = uuid2,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::ReadOnly
|
||||
.dbRole = ClioNode::DbRole::ReadOnly,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true
|
||||
};
|
||||
ClioNode const node3{
|
||||
.uuid = uuid3,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::NotWriter
|
||||
.dbRole = ClioNode::DbRole::NotWriter,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
|
||||
std::vector<ClioNode> const nodes = {node1, node2, node3};
|
||||
@@ -149,7 +155,9 @@ TEST_F(MetricsTest, OnNewStateWithSingleNode)
|
||||
ClioNode const node1{
|
||||
.uuid = uuid1,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::Writer
|
||||
.dbRole = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
|
||||
std::vector<ClioNode> const nodes = {node1};
|
||||
@@ -185,12 +193,16 @@ TEST_F(MetricsTest, OnNewStateRecoveryFromFailure)
|
||||
ClioNode const node1{
|
||||
.uuid = uuid1,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::Writer
|
||||
.dbRole = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true
|
||||
};
|
||||
ClioNode const node2{
|
||||
.uuid = uuid2,
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = ClioNode::DbRole::ReadOnly
|
||||
.dbRole = ClioNode::DbRole::ReadOnly,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false
|
||||
};
|
||||
|
||||
std::vector<ClioNode> const nodes = {node1, node2};
|
||||
|
||||
@@ -39,10 +39,17 @@ using namespace cluster;
|
||||
|
||||
enum class ExpectedAction { StartWriting, GiveUpWriting, NoAction, SetFallback };
|
||||
|
||||
struct NodeParams {
|
||||
uint8_t uuidValue;
|
||||
ClioNode::DbRole role;
|
||||
bool etlStarted = true;
|
||||
bool cacheIsFull = true;
|
||||
};
|
||||
|
||||
struct WriterDeciderTestParams {
|
||||
std::string testName;
|
||||
uint8_t selfUuidValue;
|
||||
std::vector<std::pair<uint8_t, ClioNode::DbRole>> nodes;
|
||||
std::vector<NodeParams> nodes;
|
||||
ExpectedAction expectedAction;
|
||||
bool useEmptyClusterData = false;
|
||||
};
|
||||
@@ -59,12 +66,19 @@ struct WriterDeciderTest : testing::TestWithParam<WriterDeciderTestParams> {
|
||||
MockWriterState& writerStateRef = *writerState;
|
||||
|
||||
static ClioNode
|
||||
makeNode(boost::uuids::uuid const& uuid, ClioNode::DbRole role)
|
||||
makeNode(
|
||||
boost::uuids::uuid const& uuid,
|
||||
ClioNode::DbRole role,
|
||||
bool etlStarted,
|
||||
bool cacheIsFull
|
||||
)
|
||||
{
|
||||
return ClioNode{
|
||||
.uuid = std::make_shared<boost::uuids::uuid>(uuid),
|
||||
.updateTime = std::chrono::system_clock::now(),
|
||||
.dbRole = role
|
||||
.dbRole = role,
|
||||
.etlStarted = etlStarted,
|
||||
.cacheIsFull = cacheIsFull
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,9 +139,14 @@ TEST_P(WriterDeciderTest, WriterSelection)
|
||||
} else {
|
||||
std::vector<ClioNode> nodes;
|
||||
nodes.reserve(params.nodes.size());
|
||||
for (auto const& [uuidValue, role] : params.nodes) {
|
||||
auto node = makeNode(makeUuid(uuidValue), role);
|
||||
if (uuidValue == params.selfUuidValue) {
|
||||
for (auto const& nodeParam : params.nodes) {
|
||||
auto node = makeNode(
|
||||
makeUuid(nodeParam.uuidValue),
|
||||
nodeParam.role,
|
||||
nodeParam.etlStarted,
|
||||
nodeParam.cacheIsFull
|
||||
);
|
||||
if (nodeParam.uuidValue == params.selfUuidValue) {
|
||||
selfIdPtr = node.uuid; // Use the same shared_ptr as in the node
|
||||
}
|
||||
nodes.push_back(std::move(node));
|
||||
@@ -276,47 +295,111 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
{0x04, ClioNode::DbRole::Writer}},
|
||||
.expectedAction = ExpectedAction::SetFallback
|
||||
},
|
||||
// Tests for etlStarted / cacheIsFull election logic
|
||||
WriterDeciderTestParams{
|
||||
.testName = "SelfIsLoadingCacheOtherIsWriter",
|
||||
.selfUuidValue = 0x01,
|
||||
.nodes = {{0x01, ClioNode::DbRole::LoadingCache}, {0x02, ClioNode::DbRole::Writer}},
|
||||
.expectedAction = ExpectedAction::GiveUpWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "OtherNodeIsLoadingCacheSkipToNextWriter",
|
||||
.testName = "EtlNotStartedNodeSkipped_CacheFullNodeSelected",
|
||||
.selfUuidValue = 0x02,
|
||||
.nodes =
|
||||
{{0x01, ClioNode::DbRole::LoadingCache},
|
||||
{0x02, ClioNode::DbRole::Writer},
|
||||
{0x03, ClioNode::DbRole::NotWriter}},
|
||||
{{.uuidValue = 0x01,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = true},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true},
|
||||
{.uuidValue = 0x03,
|
||||
.role = ClioNode::DbRole::NotWriter,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true}},
|
||||
.expectedAction = ExpectedAction::StartWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "AllNodesLoadingCacheNoActionTaken",
|
||||
.testName = "AllNodesEtlNotStarted_GiveUpWriting",
|
||||
.selfUuidValue = 0x01,
|
||||
.nodes =
|
||||
{{0x01, ClioNode::DbRole::LoadingCache}, {0x02, ClioNode::DbRole::LoadingCache}},
|
||||
.expectedAction = ExpectedAction::NoAction
|
||||
{{.uuidValue = 0x01,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false}},
|
||||
.expectedAction = ExpectedAction::GiveUpWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "MixedWithLoadingCacheReadOnlyFirstNonReadOnlyNonLoadingCacheSelected",
|
||||
.selfUuidValue = 0x03,
|
||||
.testName = "CacheNotFullFallsBackToEtlStartedSelection_SelfSelected",
|
||||
.selfUuidValue = 0x01,
|
||||
.nodes =
|
||||
{{0x01, ClioNode::DbRole::ReadOnly},
|
||||
{0x02, ClioNode::DbRole::LoadingCache},
|
||||
{0x03, ClioNode::DbRole::Writer},
|
||||
{0x04, ClioNode::DbRole::NotWriter}},
|
||||
{{.uuidValue = 0x01,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false}},
|
||||
.expectedAction = ExpectedAction::StartWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "LoadingCacheBeforeWriterSkipsLoadingCache",
|
||||
.testName = "CacheFullNodePreferredOverCacheNotFullNode",
|
||||
.selfUuidValue = 0x02,
|
||||
.nodes =
|
||||
{{.uuidValue = 0x01,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true},
|
||||
{.uuidValue = 0x03,
|
||||
.role = ClioNode::DbRole::NotWriter,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false}},
|
||||
.expectedAction = ExpectedAction::StartWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "CacheFullNodePreferredEvenIfHigherUuid",
|
||||
.selfUuidValue = 0x04,
|
||||
.nodes =
|
||||
{{0x01, ClioNode::DbRole::LoadingCache},
|
||||
{0x02, ClioNode::DbRole::LoadingCache},
|
||||
{0x03, ClioNode::DbRole::Writer},
|
||||
{0x04, ClioNode::DbRole::NotWriter}},
|
||||
{{.uuidValue = 0x01,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x03,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true},
|
||||
{.uuidValue = 0x04,
|
||||
.role = ClioNode::DbRole::NotWriter,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true}},
|
||||
.expectedAction = ExpectedAction::GiveUpWriting
|
||||
},
|
||||
WriterDeciderTestParams{
|
||||
.testName = "MixedWithReadOnly_EtlNotStartedNodeSkipped",
|
||||
.selfUuidValue = 0x03,
|
||||
.nodes =
|
||||
{{.uuidValue = 0x01, .role = ClioNode::DbRole::ReadOnly},
|
||||
{.uuidValue = 0x02,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = false,
|
||||
.cacheIsFull = false},
|
||||
{.uuidValue = 0x03,
|
||||
.role = ClioNode::DbRole::Writer,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true},
|
||||
{.uuidValue = 0x04,
|
||||
.role = ClioNode::DbRole::NotWriter,
|
||||
.etlStarted = true,
|
||||
.cacheIsFull = true}},
|
||||
.expectedAction = ExpectedAction::StartWriting
|
||||
}
|
||||
),
|
||||
[](testing::TestParamInfo<WriterDeciderTestParams> const& info) { return info.param.testName; }
|
||||
|
||||
@@ -209,7 +209,7 @@ TEST_P(ParametrizedCacheLoaderTest, LoadCacheWithDifferentSettings)
|
||||
.WillRepeatedly(Return(std::vector<Blob>(keysSize - 1, Blob{'s'})));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, updateImp).Times(loops);
|
||||
EXPECT_CALL(cache, updateImpl).Times(loops);
|
||||
EXPECT_CALL(cache, setFull).Times(1);
|
||||
|
||||
async::CoroExecutionContext ctx{settings.numThreads};
|
||||
@@ -244,7 +244,7 @@ TEST_P(ParametrizedCacheLoaderTest, AutomaticallyCancelledAndAwaitedInDestructor
|
||||
.WillRepeatedly(Return(std::vector<Blob>(keysSize - 1, Blob{'s'})));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, updateImp).Times(AtMost(loops));
|
||||
EXPECT_CALL(cache, updateImpl).Times(AtMost(loops));
|
||||
EXPECT_CALL(cache, setFull).Times(AtMost(1));
|
||||
|
||||
async::CoroExecutionContext ctx{settings.numThreads};
|
||||
@@ -279,7 +279,7 @@ TEST_P(ParametrizedCacheLoaderTest, CacheDisabledLeadsToCancellation)
|
||||
.WillRepeatedly(Return(std::vector<Blob>(keysSize - 1, Blob{'s'})));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillOnce(Return(false)).WillRepeatedly(Return(true));
|
||||
EXPECT_CALL(cache, updateImp).Times(AtMost(1));
|
||||
EXPECT_CALL(cache, updateImpl).Times(AtMost(1));
|
||||
EXPECT_CALL(cache, setFull).Times(0);
|
||||
|
||||
async::CoroExecutionContext ctx{settings.numThreads};
|
||||
@@ -320,7 +320,7 @@ TEST_F(CacheLoaderTest, SyncCacheLoaderWaitsTillFullyLoaded)
|
||||
.WillRepeatedly(Return(std::vector<Blob>{keysSize - 1, Blob{'s'}}));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, updateImp).Times(loops);
|
||||
EXPECT_CALL(cache, updateImpl).Times(loops);
|
||||
EXPECT_CALL(cache, isFull).WillOnce(Return(false)).WillRepeatedly(Return(true));
|
||||
EXPECT_CALL(cache, setFull).Times(1);
|
||||
|
||||
@@ -346,7 +346,7 @@ TEST_F(CacheLoaderTest, AsyncCacheLoaderCanBeStopped)
|
||||
.WillRepeatedly(Return(std::vector<Blob>{keysSize - 1, Blob{'s'}}));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, updateImp).Times(AtMost(loops));
|
||||
EXPECT_CALL(cache, updateImpl).Times(AtMost(loops));
|
||||
EXPECT_CALL(cache, isFull).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, setFull).Times(AtMost(1));
|
||||
|
||||
@@ -360,7 +360,7 @@ TEST_F(CacheLoaderTest, DisabledCacheLoaderDoesNotLoadCache)
|
||||
auto const cfg = getParseCacheConfig(json::parse(R"JSON({"cache": {"load": "none"}})JSON"));
|
||||
CacheLoader loader{cfg, backend_, cache};
|
||||
|
||||
EXPECT_CALL(cache, updateImp).Times(0);
|
||||
EXPECT_CALL(cache, updateImpl).Times(0);
|
||||
EXPECT_CALL(cache, isFull).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, setDisabled).Times(1);
|
||||
|
||||
@@ -372,7 +372,7 @@ TEST_F(CacheLoaderTest, DisabledCacheLoaderCanCallStopAndWait)
|
||||
auto const cfg = getParseCacheConfig(json::parse(R"JSON({"cache": {"load": "none"}})JSON"));
|
||||
CacheLoader loader{cfg, backend_, cache};
|
||||
|
||||
EXPECT_CALL(cache, updateImp).Times(0);
|
||||
EXPECT_CALL(cache, updateImpl).Times(0);
|
||||
EXPECT_CALL(cache, isFull).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, setDisabled).Times(1);
|
||||
|
||||
@@ -410,11 +410,12 @@ TEST_F(CacheLoaderFromFileTest, Success)
|
||||
EXPECT_CALL(cache, loadFromFile(filePath, kSEQ - maxSequenceLag))
|
||||
.WillOnce(Return(std::expected<void, std::string>{}));
|
||||
EXPECT_CALL(cache, latestLedgerSequence).WillOnce(Return(kLOADED_SEQ));
|
||||
EXPECT_CALL(cache, setFull);
|
||||
|
||||
loader.load(kSEQ);
|
||||
|
||||
std::optional<LedgerRange> const expectedLedgerRange =
|
||||
LedgerRange{.minSequence = kSEQ - 20, .maxSequence = kLOADED_SEQ};
|
||||
LedgerRange{.minSequence = kSEQ - 20, .maxSequence = kSEQ};
|
||||
EXPECT_EQ(backend_->fetchLedgerRange(), expectedLedgerRange);
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ TEST_F(CacheLoaderFromFileTest, FailureBackToNormalLoad)
|
||||
.WillRepeatedly(Return(std::vector<Blob>{keysSize - 1, Blob{'s'}}));
|
||||
|
||||
EXPECT_CALL(cache, isDisabled).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(cache, updateImp).Times(loops);
|
||||
EXPECT_CALL(cache, updateImpl).Times(loops);
|
||||
EXPECT_CALL(cache, isFull).WillOnce(Return(false)).WillRepeatedly(Return(true));
|
||||
EXPECT_CALL(cache, setFull).Times(1);
|
||||
|
||||
@@ -469,6 +470,32 @@ TEST_F(CacheLoaderFromFileTest, MaxSequenceLagCalculation)
|
||||
loader.load(kSEQ);
|
||||
}
|
||||
|
||||
TEST_F(CacheLoaderFromFileTest, FileSequenceBehindBackendFetchesMissingLedgersFromDB)
|
||||
{
|
||||
constexpr uint32_t kFILE_SEQ = kSEQ - 2;
|
||||
auto const diffs = diffProvider.getLatestDiff();
|
||||
|
||||
EXPECT_CALL(cache, isFull).WillOnce(Return(false));
|
||||
EXPECT_CALL(cache, loadFromFile(filePath, kSEQ - maxSequenceLag))
|
||||
.WillOnce(Return(std::expected<void, std::string>{}));
|
||||
|
||||
// latestLedgerSequence is called twice per loop iteration (condition + seqToLoad + 1)
|
||||
// plus once for the final exit check
|
||||
EXPECT_CALL(cache, latestLedgerSequence)
|
||||
.WillOnce(Return(kFILE_SEQ)) // iteration 1: condition (true)
|
||||
.WillOnce(Return(kFILE_SEQ)) // iteration 1: seqToLoad + 1 = kFILE_SEQ + 1
|
||||
.WillOnce(Return(kFILE_SEQ + 1)) // iteration 2: condition (true)
|
||||
.WillOnce(Return(kFILE_SEQ + 1)) // iteration 2: seqToLoad + 1 = kFILE_SEQ + 2
|
||||
.WillOnce(Return(kSEQ)); // exit condition (false)
|
||||
|
||||
EXPECT_CALL(*backend_, fetchLedgerDiff(kFILE_SEQ + 1, _)).WillOnce(Return(diffs));
|
||||
EXPECT_CALL(*backend_, fetchLedgerDiff(kFILE_SEQ + 2, _)).WillOnce(Return(diffs));
|
||||
EXPECT_CALL(cache, updateImpl).Times(2);
|
||||
EXPECT_CALL(cache, setFull).Times(1);
|
||||
|
||||
loader.load(kSEQ);
|
||||
}
|
||||
|
||||
TEST_F(CacheLoaderFromFileTest, MaxSequenceLagClampedToMinOfLedgerRange)
|
||||
{
|
||||
uint32_t const currentSeq = 110;
|
||||
|
||||
@@ -325,7 +325,7 @@ TEST_F(ETLServiceTests, RunWithEmptyDatabase)
|
||||
auto mockTaskManager = std::make_unique<testing::NiceMock<MockTaskManager>>();
|
||||
auto& mockTaskManagerRef = *mockTaskManager;
|
||||
auto ledgerData = createTestData(kSEQ);
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
|
||||
testing::Sequence const s;
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
@@ -336,20 +336,19 @@ TEST_F(ETLServiceTests, RunWithEmptyDatabase)
|
||||
EXPECT_CALL(*balancer_, loadInitialLedger(kSEQ, testing::_, testing::_))
|
||||
.WillOnce(testing::Return(std::vector<std::string>{}));
|
||||
EXPECT_CALL(*loader_, loadInitialLedger).WillOnce(testing::Return(ripple::LedgerHeader{}));
|
||||
// In syncCacheWithDb()
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange).Times(2).InSequence(s).WillRepeatedly([this]() {
|
||||
backend_->cache().update({}, kSEQ, false);
|
||||
return data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ};
|
||||
});
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.Times(1)
|
||||
.InSequence(s)
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(mockTaskManagerRef, run);
|
||||
EXPECT_CALL(*taskManagerProvider_, make(testing::_, testing::_, kSEQ + 1, testing::_))
|
||||
.WillOnce([&](auto&&...) {
|
||||
EXPECT_FALSE(systemState_->isLoadingCache);
|
||||
EXPECT_TRUE(systemState_->etlStarted);
|
||||
return std::unique_ptr<etl::TaskManagerInterface>(mockTaskManager.release());
|
||||
});
|
||||
EXPECT_CALL(*monitorProvider_, make(testing::_, testing::_, testing::_, kSEQ + 1, testing::_))
|
||||
.WillOnce([this](auto, auto, auto, auto, auto) {
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
return std::make_unique<testing::NiceMock<MockMonitor>>();
|
||||
});
|
||||
|
||||
@@ -358,13 +357,13 @@ TEST_F(ETLServiceTests, RunWithEmptyDatabase)
|
||||
|
||||
TEST_F(ETLServiceTests, RunWithPopulatedDatabase)
|
||||
{
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
backend_->cache().update({}, kSEQ, false);
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.WillRepeatedly(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*monitorProvider_, make(testing::_, testing::_, testing::_, kSEQ + 1, testing::_))
|
||||
.WillOnce([this](auto, auto, auto, auto, auto) {
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
return std::make_unique<testing::NiceMock<MockMonitor>>();
|
||||
});
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillRepeatedly(testing::Return(kSEQ));
|
||||
@@ -375,21 +374,14 @@ TEST_F(ETLServiceTests, RunWithPopulatedDatabase)
|
||||
|
||||
TEST_F(ETLServiceTests, SyncCacheWithDbBeforeStartingMonitor)
|
||||
{
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
backend_->cache().update({}, kSEQ - 2, false);
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.WillRepeatedly(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
|
||||
EXPECT_CALL(*backend_, fetchLedgerDiff(kSEQ - 1, testing::_));
|
||||
EXPECT_CALL(*cacheUpdater_, update(kSEQ - 1, std::vector<data::LedgerObject>()))
|
||||
.WillOnce([this](auto const seq, auto&&...) { backend_->cache().update({}, seq, false); });
|
||||
EXPECT_CALL(*backend_, fetchLedgerDiff(kSEQ, testing::_));
|
||||
EXPECT_CALL(*cacheUpdater_, update(kSEQ, std::vector<data::LedgerObject>()))
|
||||
.WillOnce([this](auto const seq, auto&&...) { backend_->cache().update({}, seq, false); });
|
||||
|
||||
EXPECT_CALL(*monitorProvider_, make(testing::_, testing::_, testing::_, kSEQ + 1, testing::_))
|
||||
.WillOnce([this](auto, auto, auto, auto, auto) {
|
||||
EXPECT_TRUE(systemState_->isLoadingCache);
|
||||
EXPECT_FALSE(systemState_->etlStarted);
|
||||
return std::make_unique<testing::NiceMock<MockMonitor>>();
|
||||
});
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillRepeatedly(testing::Return(kSEQ));
|
||||
@@ -433,7 +425,6 @@ TEST_F(ETLServiceTests, HandlesWriteConflictInMonitorSubscription)
|
||||
// Set cache to be in sync with DB to avoid syncCacheWithDb loop
|
||||
backend_->cache().update({}, kSEQ, false);
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillOnce(testing::Return(kSEQ));
|
||||
EXPECT_CALL(*cacheLoader_, load(kSEQ));
|
||||
@@ -470,7 +461,6 @@ TEST_F(ETLServiceTests, NormalFlowInMonitorSubscription)
|
||||
// Set cache to be in sync with DB to avoid syncCacheWithDb loop
|
||||
backend_->cache().update({}, kSEQ, false);
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillOnce(testing::Return(kSEQ));
|
||||
EXPECT_CALL(*cacheLoader_, load(kSEQ));
|
||||
@@ -565,7 +555,6 @@ TEST_F(ETLServiceTests, GiveUpWriterAfterWriteConflict)
|
||||
// Set cache to be in sync with DB to avoid syncCacheWithDb loop
|
||||
backend_->cache().update({}, kSEQ, false);
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillOnce(testing::Return(kSEQ));
|
||||
EXPECT_CALL(*cacheLoader_, load(kSEQ));
|
||||
@@ -782,13 +771,8 @@ TEST_F(ETLServiceTests, OnlyCacheUpdatesWhenBackendIsCurrent)
|
||||
EXPECT_CALL(mockMonitorRef, subscribeToDbStalled);
|
||||
EXPECT_CALL(mockMonitorRef, run);
|
||||
|
||||
// Set backend range to be at kSEQ + 1 (already current)
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}))
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}))
|
||||
.WillRepeatedly(
|
||||
testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ + 1})
|
||||
);
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillOnce(testing::Return(kSEQ));
|
||||
EXPECT_CALL(*cacheLoader_, load(kSEQ));
|
||||
|
||||
@@ -831,13 +815,8 @@ TEST_F(ETLServiceTests, NoUpdatesWhenBothCacheAndBackendAreCurrent)
|
||||
EXPECT_CALL(mockMonitorRef, subscribeToDbStalled);
|
||||
EXPECT_CALL(mockMonitorRef, run);
|
||||
|
||||
// Set backend range to be at kSEQ + 1 (already current)
|
||||
EXPECT_CALL(*backend_, hardFetchLedgerRange)
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}))
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}))
|
||||
.WillRepeatedly(
|
||||
testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ + 1})
|
||||
);
|
||||
.WillOnce(testing::Return(data::LedgerRange{.minSequence = 1, .maxSequence = kSEQ}));
|
||||
EXPECT_CALL(*ledgers_, getMostRecent()).WillOnce(testing::Return(kSEQ));
|
||||
EXPECT_CALL(*cacheLoader_, load(kSEQ));
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ TEST_F(SystemStateTest, InitialValuesAreCorrect)
|
||||
|
||||
EXPECT_FALSE(state.isStrictReadonly);
|
||||
EXPECT_FALSE(state.isWriting);
|
||||
EXPECT_TRUE(state.isLoadingCache);
|
||||
EXPECT_FALSE(state.etlStarted);
|
||||
EXPECT_FALSE(state.isAmendmentBlocked);
|
||||
EXPECT_FALSE(state.isCorruptionDetected);
|
||||
EXPECT_FALSE(state.isWriterDecidingFallback);
|
||||
@@ -66,7 +66,7 @@ TEST_P(SystemStateReadOnlyTest, MakeSystemStateWithReadOnly)
|
||||
|
||||
EXPECT_EQ(state->isStrictReadonly, readOnlyValue);
|
||||
EXPECT_FALSE(state->isWriting);
|
||||
EXPECT_TRUE(state->isLoadingCache);
|
||||
EXPECT_FALSE(state->etlStarted);
|
||||
EXPECT_FALSE(state->isAmendmentBlocked);
|
||||
EXPECT_FALSE(state->isCorruptionDetected);
|
||||
EXPECT_FALSE(state->isWriterDecidingFallback);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "etl/SystemState.hpp"
|
||||
#include "etl/WriterState.hpp"
|
||||
#include "util/MockLedgerCache.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
@@ -32,7 +33,8 @@ using namespace testing;
|
||||
struct WriterStateTest : util::prometheus::WithPrometheus {
|
||||
std::shared_ptr<SystemState> systemState = std::make_shared<SystemState>();
|
||||
StrictMock<MockFunction<void(SystemState::WriteCommand)>> mockWriteCommand;
|
||||
WriterState writerState{systemState};
|
||||
NiceMock<MockLedgerCache> cache;
|
||||
WriterState writerState{systemState, cache};
|
||||
|
||||
WriterStateTest()
|
||||
{
|
||||
@@ -117,27 +119,36 @@ TEST_F(WriterStateTest, IsReadOnlyReturnsSystemStateValue)
|
||||
EXPECT_TRUE(writerState.isReadOnly());
|
||||
}
|
||||
|
||||
TEST_F(WriterStateTest, IsLoadingCacheReturnsSystemStateValue)
|
||||
TEST_F(WriterStateTest, IsEtlStartedReturnsSystemStateValue)
|
||||
{
|
||||
systemState->isLoadingCache = false;
|
||||
EXPECT_FALSE(writerState.isLoadingCache());
|
||||
systemState->etlStarted = false;
|
||||
EXPECT_FALSE(writerState.isEtlStarted());
|
||||
|
||||
systemState->isLoadingCache = true;
|
||||
EXPECT_TRUE(writerState.isLoadingCache());
|
||||
systemState->etlStarted = true;
|
||||
EXPECT_TRUE(writerState.isEtlStarted());
|
||||
}
|
||||
|
||||
TEST_F(WriterStateTest, IsCacheFullReturnsCacheValue)
|
||||
{
|
||||
EXPECT_CALL(cache, isFull()).WillOnce(Return(false));
|
||||
EXPECT_FALSE(writerState.isCacheFull());
|
||||
|
||||
EXPECT_CALL(cache, isFull()).WillOnce(Return(true));
|
||||
EXPECT_TRUE(writerState.isCacheFull());
|
||||
}
|
||||
|
||||
TEST_F(WriterStateTest, CloneCreatesNewInstanceWithSameSystemState)
|
||||
{
|
||||
systemState->isWriting = true;
|
||||
systemState->isStrictReadonly = true;
|
||||
systemState->isLoadingCache = false;
|
||||
systemState->etlStarted = false;
|
||||
|
||||
auto cloned = writerState.clone();
|
||||
|
||||
ASSERT_NE(cloned.get(), &writerState);
|
||||
EXPECT_TRUE(cloned->isWriting());
|
||||
EXPECT_TRUE(cloned->isReadOnly());
|
||||
EXPECT_FALSE(cloned->isLoadingCache());
|
||||
EXPECT_FALSE(cloned->isEtlStarted());
|
||||
}
|
||||
|
||||
TEST_F(WriterStateTest, ClonedInstanceSharesSystemState)
|
||||
|
||||
Reference in New Issue
Block a user