fix: Use a single timestamp in server_info (#3234)

Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>
This commit is contained in:
Ayaz Salikhov
2026-09-28 16:49:04 +03:00
committed by GitHub
parent 7f88c51593
commit 33241a9b85
4 changed files with 143 additions and 44 deletions

View File

@@ -8,6 +8,7 @@
#include "rpc/JS.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include "util/Concepts.hpp"
#include "util/build/Build.hpp"
#include <boost/json/conversion.hpp>
@@ -43,8 +44,9 @@ namespace rpc {
* @brief Contains common functionality for handling the `server_info` command
*
* @tparam CountersType The type of the counters
* @tparam ClockType Clock used for the output time and the ledger age
*/
template <typename CountersType>
template <typename CountersType, util::SomeSystemClock ClockType = std::chrono::system_clock>
class BaseServerInfoHandler
: public rpc::spec::HandlerFor<rpc::spec::handlers::server_info::Input> {
static constexpr auto kBackendCountersKey = "backend_counters";
@@ -95,7 +97,7 @@ public:
std::optional<AdminSection> adminSection = std::nullopt;
std::string completeLedgers;
uint32_t loadFactor = 1u;
std::chrono::time_point<std::chrono::system_clock> time = std::chrono::system_clock::now();
std::chrono::time_point<std::chrono::system_clock> time = ClockType::now();
std::chrono::seconds uptime = {};
std::string clioVersion = util::build::getClioVersionString();
std::string xrplVersion = xrpl::build_info::getVersionString();
@@ -171,8 +173,7 @@ public:
return Error{Status{RippledError::RpcInternal}};
auto output = Output{};
auto const sinceEpoch =
duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
auto const sinceEpoch = duration_cast<seconds>(output.info.time.time_since_epoch()).count();
auto const age = static_cast<int32_t>(sinceEpoch) -
static_cast<int32_t>(lgrInfo->closeTime.time_since_epoch().count()) -
static_cast<int32_t>(kRippleEpochStart);

View File

@@ -2,6 +2,8 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <concepts>
#include <cstddef>
#include <string_view>
#include <type_traits>
@@ -14,6 +16,14 @@ namespace util {
template <typename T>
concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>;
/**
* @brief Specifies a clock that reports the current time as a system clock time point
*/
template <typename T>
concept SomeSystemClock = requires {
{ T::now() } -> std::same_as<std::chrono::system_clock::time_point>;
};
/**
* @brief Checks that the list of given values contains no duplicates
*

View File

@@ -0,0 +1,56 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
/**
* @brief A clock that always reports the same instant and counts how often it was read.
*
* Satisfies util::SomeSystemClock, so it can stand in for std::chrono::system_clock in any
* component templated on a clock. The call count makes "the clock was sampled exactly once"
* an assertable property, and the fixed instant makes time-derived output deterministic.
*
* The counter is process-wide: reset it in the fixture constructor of every suite that reads it.
*/
class TestConstantClock {
public:
/** @brief The instant now() reports, as a Unix timestamp in seconds. */
static constexpr std::uint32_t kNowUnix = 1'700'000'000u;
/** @brief The instant now() reports. */
static constexpr std::chrono::system_clock::time_point kNow{std::chrono::seconds{kNowUnix}};
/**
* @brief Report the fixed instant and count the read
*
* @return kNow
*/
static std::chrono::system_clock::time_point
now()
{
++callCounter;
return kNow;
}
/**
* @brief How often now() has been called since the last reset
*
* @return The call count
*/
static std::size_t
callCount()
{
return callCounter;
}
/** @brief Set the call count back to zero. */
static void
resetCounter()
{
callCounter = 0;
}
private:
static inline std::size_t callCounter = 0;
};

View File

@@ -9,6 +9,7 @@
#include "util/MockETLServiceTestFixture.hpp"
#include "util/MockLoadBalancer.hpp"
#include "util/MockSubscriptionManager.hpp"
#include "util/TestConstantClock.hpp"
#include "util/TestObject.hpp"
#include <boost/json/object.hpp>
@@ -18,6 +19,8 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <rpcspec/Errors.hpp>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <chrono>
#include <optional>
@@ -27,21 +30,50 @@ using namespace rpc;
using namespace data;
using namespace testing;
using TestServerInfoHandler = BaseServerInfoHandler<MockCounters>;
namespace {
constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto kClientIp = "1.1.1.1";
constexpr auto kNowUnix = TestConstantClock::kNowUnix;
using TestServerInfoHandler = BaseServerInfoHandler<MockCounters, TestConstantClock>;
} // namespace
struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest {
RPCServerInfoHandlerTest()
{
TestConstantClock::resetCounter();
backend_->setRange(10, 30);
}
template <typename Callback>
void
runNormalRequest(xrpl::LedgerHeader const& ledgerHeader, Callback callback)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
EXPECT_CALL(*backend_, doFetchLedgerObject)
.WillOnce(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
EXPECT_CALL(*mockLoadBalancerPtr_, forwardToRippled(_, Eq(kClientIp), false, _))
.WillOnce(Return(std::unexpected{rpc::ClioError::RpcForwardingInvalidResponse}));
EXPECT_CALL(*mockCountersPtr_, uptime).WillOnce(Return(std::chrono::seconds{1234}));
EXPECT_CALL(*mockETLServicePtr_, isAmendmentBlocked).WillOnce(Return(false));
auto const handler = AnyHandler{TestServerInfoHandler{
backend_,
mockSubscriptionManagerPtr_,
mockLoadBalancerPtr_,
mockETLServicePtr_,
*mockCountersPtr_
}};
runSpawn([&](auto yield) {
callback(
handler.process(boost::json::parse("{}"), Context{yield, {}, false, kClientIp})
);
});
}
static void
validateNormalOutput(rpc::ReturnType const& output)
{
@@ -57,6 +89,12 @@ struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCou
EXPECT_TRUE(info.contains("libxrpl_version"));
EXPECT_TRUE(info.contains("validated_ledger"));
EXPECT_TRUE(info.contains("time"));
EXPECT_EQ(
boost::json::value_to<std::string>(info.at("time")),
xrpl::to_string(
std::chrono::time_point_cast<std::chrono::microseconds>(TestConstantClock::kNow)
)
);
EXPECT_TRUE(info.contains("uptime"));
auto const& validated = info.at("validated_ledger").as_object();
@@ -135,6 +173,7 @@ TEST_F(RPCServerInfoHandlerTest, NoLedgerHeaderErrorsOutWithInternal)
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "internal");
EXPECT_EQ(err.at("error_message").as_string(), "Internal error.");
EXPECT_EQ(TestConstantClock::callCount(), 0u);
});
}
@@ -160,42 +199,14 @@ TEST_F(RPCServerInfoHandlerTest, NoFeesErrorsOutWithInternal)
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "internal");
EXPECT_EQ(err.at("error_message").as_string(), "Internal error.");
EXPECT_EQ(TestConstantClock::callCount(), 0u);
});
}
TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
{
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob));
EXPECT_CALL(
*rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_)
)
.WillOnce(Return(std::unexpected{rpc::ClioError::RpcForwardingInvalidResponse}));
EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234}));
EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false));
auto const handler = AnyHandler{TestServerInfoHandler{
backend_,
mockSubscriptionManagerPtr_,
mockLoadBalancerPtr_,
mockETLServicePtr_,
*mockCountersPtr_
}};
runSpawn([&](auto yield) {
auto const req = boost::json::parse("{}");
auto const output = handler.process(req, Context{yield, {}, false, kClientIp});
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
runNormalRequest(ledgerHeader, [&](auto const& output) {
validateNormalOutput(output);
// no admin section present by default
@@ -206,13 +217,34 @@ TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
});
}
TEST_F(RPCServerInfoHandlerTest, SamplesTheClockOnceForTimeAndAge)
{
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
runNormalRequest(ledgerHeader, [&](auto const& output) {
ASSERT_TRUE(output);
EXPECT_EQ(TestConstantClock::callCount(), 1u);
});
}
TEST_F(RPCServerInfoHandlerTest, FutureLedgerCloseTimeReportsZeroAge)
{
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix + 5);
runNormalRequest(ledgerHeader, [&](auto const& output) {
ASSERT_TRUE(output);
auto const& result = output.result.value().as_object();
auto const& info = result.at("info").as_object();
auto const& validated = info.at("validated_ledger").as_object();
EXPECT_EQ(validated.at("age").as_uint64(), 0u);
});
}
TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet)
{
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -253,7 +285,7 @@ TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet)
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -293,7 +325,7 @@ TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly)
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2).WillRepeatedly(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -351,7 +383,7 @@ TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -394,7 +426,7 @@ TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -444,7 +476,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
@@ -498,7 +530,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);