From cb26308ba37308ed313951d671fa0dd913fdaa86 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:28:26 +0000 Subject: [PATCH] refactore --- include/xrpl/server/detail/Door.h | 20 +-- .../xrpl/server/detail/ExponentialBackoff.h | 93 ++++++++++ src/test/server/ExponentialBackoff_test.cpp | 162 ++++++++++++++++++ src/xrpld/app/main/GRPCServer.cpp | 16 +- src/xrpld/app/main/GRPCServer.h | 5 +- 5 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 include/xrpl/server/detail/ExponentialBackoff.h create mode 100644 src/test/server/ExponentialBackoff_test.cpp diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index 4d623a286f..21398c2bc5 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -83,9 +84,7 @@ private: boost::asio::strand strand_; bool ssl_; bool plain_; - static constexpr std::chrono::milliseconds INITIAL_ACCEPT_DELAY{50}; - static constexpr std::chrono::milliseconds MAX_ACCEPT_DELAY{2000}; - std::chrono::milliseconds accept_delay_{INITIAL_ACCEPT_DELAY}; + ExponentialBackoff backoff_; boost::asio::steady_timer backoff_timer_; void @@ -317,11 +316,11 @@ Door::do_accept(boost::asio::yield_context do_yield) { if (FDGuard::should_throttle(0.70)) { - backoff_timer_.expires_after(accept_delay_); + backoff_timer_.expires_after(backoff_.current()); boost::system::error_code tec; backoff_timer_.async_wait(do_yield[tec]); - accept_delay_ = std::min(accept_delay_ * 2, MAX_ACCEPT_DELAY); - JLOG(j_.warn()) << "Throttling do_accept for " << accept_delay_.count() << "ms."; + auto const delay = backoff_.increase(); + JLOG(j_.warn()) << "Throttling do_accept for " << delay.count() << "ms."; continue; } @@ -338,14 +337,15 @@ Door::do_accept(boost::asio::yield_context do_yield) if (ec == boost::asio::error::no_descriptors || ec == boost::asio::error::no_buffer_space) { + auto const delay = backoff_.current(); JLOG(j_.warn()) << "accept: Too many open files. Pausing for " - << accept_delay_.count() << "ms."; + << delay.count() << "ms."; - backoff_timer_.expires_after(accept_delay_); + backoff_timer_.expires_after(delay); boost::system::error_code tec; backoff_timer_.async_wait(do_yield[tec]); - accept_delay_ = std::min(accept_delay_ * 2, MAX_ACCEPT_DELAY); + backoff_.increase(); } else { @@ -354,7 +354,7 @@ Door::do_accept(boost::asio::yield_context do_yield) continue; } - accept_delay_ = INITIAL_ACCEPT_DELAY; + backoff_.reset(); if (ssl_ && plain_) { diff --git a/include/xrpl/server/detail/ExponentialBackoff.h b/include/xrpl/server/detail/ExponentialBackoff.h new file mode 100644 index 0000000000..84aec9f7b3 --- /dev/null +++ b/include/xrpl/server/detail/ExponentialBackoff.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +namespace xrpl { + +/** + * @brief Exponential backoff delay manager with configurable limits. + * + * Manages delay values that double on each increase (capped at maximum) + * and can be reset to initial value. Used for throttling accept() calls + * when file descriptor pressure is detected. + */ +class ExponentialBackoff +{ +public: + using duration_type = std::chrono::milliseconds; + + static constexpr duration_type DEFAULT_INITIAL_DELAY{50}; + static constexpr duration_type DEFAULT_MAX_DELAY{2000}; + + /** + * @brief Construct with custom or default delay parameters. + * + * @param initial Initial delay value (default: 50ms) + * @param maximum Maximum delay cap (default: 2000ms) + */ + explicit ExponentialBackoff( + duration_type initial = DEFAULT_INITIAL_DELAY, + duration_type maximum = DEFAULT_MAX_DELAY) + : initial_(initial), maximum_(maximum), current_(initial) + { + } + + /** + * @brief Get current delay value. + */ + [[nodiscard]] duration_type + current() const noexcept + { + return current_; + } + + /** + * @brief Double the current delay, capped at maximum. + * + * @return The new current delay value after increase. + */ + duration_type + increase() noexcept + { + current_ = std::min(current_ * 2, maximum_); + return current_; + } + + /** + * @brief Reset delay to initial value. + * + * @return The initial delay value. + */ + duration_type + reset() noexcept + { + current_ = initial_; + return current_; + } + + /** + * @brief Get initial delay value. + */ + [[nodiscard]] duration_type + initial() const noexcept + { + return initial_; + } + + /** + * @brief Get maximum delay value. + */ + [[nodiscard]] duration_type + maximum() const noexcept + { + return maximum_; + } + +private: + duration_type const initial_; + duration_type const maximum_; + duration_type current_; +}; + +} // namespace xrpl diff --git a/src/test/server/ExponentialBackoff_test.cpp b/src/test/server/ExponentialBackoff_test.cpp new file mode 100644 index 0000000000..5b6b76a3a3 --- /dev/null +++ b/src/test/server/ExponentialBackoff_test.cpp @@ -0,0 +1,162 @@ +#include +#include + +namespace xrpl { + +class ExponentialBackoff_test : public beast::unit_test::suite +{ +public: + void + testDefaultConstruction() + { + testcase("default construction"); + + ExponentialBackoff backoff; + + BEAST_EXPECT(backoff.initial() == ExponentialBackoff::DEFAULT_INITIAL_DELAY); + BEAST_EXPECT(backoff.maximum() == ExponentialBackoff::DEFAULT_MAX_DELAY); + BEAST_EXPECT(backoff.current() == ExponentialBackoff::DEFAULT_INITIAL_DELAY); + } + + void + testCustomConstruction() + { + testcase("custom construction"); + + using namespace std::chrono_literals; + + ExponentialBackoff backoff{100ms, 5000ms}; + + BEAST_EXPECT(backoff.initial() == 100ms); + BEAST_EXPECT(backoff.maximum() == 5000ms); + BEAST_EXPECT(backoff.current() == 100ms); + } + + void + testIncreaseDoublesDelay() + { + testcase("increase doubles delay"); + + using namespace std::chrono_literals; + + ExponentialBackoff backoff{50ms, 2000ms}; + + BEAST_EXPECT(backoff.current() == 50ms); + + auto delay = backoff.increase(); + BEAST_EXPECT(delay == 100ms); + BEAST_EXPECT(backoff.current() == 100ms); + + delay = backoff.increase(); + BEAST_EXPECT(delay == 200ms); + BEAST_EXPECT(backoff.current() == 200ms); + + delay = backoff.increase(); + BEAST_EXPECT(delay == 400ms); + BEAST_EXPECT(backoff.current() == 400ms); + + delay = backoff.increase(); + BEAST_EXPECT(delay == 800ms); + BEAST_EXPECT(backoff.current() == 800ms); + + delay = backoff.increase(); + BEAST_EXPECT(delay == 1600ms); + BEAST_EXPECT(backoff.current() == 1600ms); + } + + void + testIncreaseCapsAtMaximum() + { + testcase("increase caps at maximum"); + + using namespace std::chrono_literals; + + ExponentialBackoff backoff{50ms, 2000ms}; + + // Increase until we hit the cap + for (int i = 0; i < 10; ++i) + { + backoff.increase(); + } + + // Should be capped at maximum + BEAST_EXPECT(backoff.current() == 2000ms); + + // Further increases should not exceed maximum + auto delay = backoff.increase(); + BEAST_EXPECT(delay == 2000ms); + BEAST_EXPECT(backoff.current() == 2000ms); + } + + void + testResetReturnsToInitial() + { + testcase("reset returns to initial"); + + using namespace std::chrono_literals; + + ExponentialBackoff backoff{50ms, 2000ms}; + + // Increase several times + backoff.increase(); + backoff.increase(); + backoff.increase(); + BEAST_EXPECT(backoff.current() == 400ms); + + // Reset should return to initial + auto delay = backoff.reset(); + BEAST_EXPECT(delay == 50ms); + BEAST_EXPECT(backoff.current() == 50ms); + } + + void + testTypicalDoorUsage() + { + testcase("typical door usage pattern"); + + using namespace std::chrono_literals; + + // Simulates Door's usage pattern + ExponentialBackoff backoff{50ms, 2000ms}; + + // First throttle + BEAST_EXPECT(backoff.current() == 50ms); + backoff.increase(); + BEAST_EXPECT(backoff.current() == 100ms); + + // Second throttle + backoff.increase(); + BEAST_EXPECT(backoff.current() == 200ms); + + // Success - reset + backoff.reset(); + BEAST_EXPECT(backoff.current() == 50ms); + + // Another throttle sequence + backoff.increase(); + BEAST_EXPECT(backoff.current() == 100ms); + backoff.increase(); + BEAST_EXPECT(backoff.current() == 200ms); + backoff.increase(); + BEAST_EXPECT(backoff.current() == 400ms); + + // Success - reset + backoff.reset(); + BEAST_EXPECT(backoff.current() == 50ms); + } + + void + run() override + { + testDefaultConstruction(); + testCustomConstruction(); + testIncreaseDoublesDelay(); + testIncreaseCapsAtMaximum(); + testResetReturnsToInitial(); + testTypicalDoorUsage(); + } +}; + +BEAST_DEFINE_TESTSUITE(ExponentialBackoff, server, xrpl); + +} // namespace xrpl diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index e9d45f53bb..196d793b25 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -457,14 +457,14 @@ GRPCServerImpl::handleRpcs() { backoffScheduled_ = true; - auto deadline = std::chrono::system_clock::now() + acceptDelay_; + auto deadline = std::chrono::system_clock::now() + backoff_.current(); backoffAlarm_.Set(cq_.get(), deadline, static_cast(&backoffTag_)); - acceptDelay_ = std::min(acceptDelay_ * 2, MAX_ACCEPT_DELAY); + auto const delay = backoff_.increase(); JLOG(journal_.warn()) - << "Scheduled backoff alarm for " << acceptDelay_.count() << "ms"; + << "Scheduled backoff alarm for " << delay.count() << "ms"; } } @@ -474,7 +474,7 @@ GRPCServerImpl::handleRpcs() else { // Not throttled - reset delay and clone immediately - acceptDelay_ = INITIAL_ACCEPT_DELAY; + backoff_.reset(); // ptr is now processing a request, so create a new CallData // object to handle additional requests @@ -520,13 +520,13 @@ GRPCServerImpl::onBackoffFired() { backoffScheduled_ = true; - auto deadline = std::chrono::system_clock::now() + acceptDelay_; + auto deadline = std::chrono::system_clock::now() + backoff_.current(); backoffAlarm_.Set(cq_.get(), deadline, static_cast(&backoffTag_)); - acceptDelay_ = std::min(acceptDelay_ * 2, MAX_ACCEPT_DELAY); + auto const delay = backoff_.increase(); - JLOG(journal_.warn()) << "Rescheduled backoff alarm for " << acceptDelay_.count() << "ms"; + JLOG(journal_.warn()) << "Rescheduled backoff alarm for " << delay.count() << "ms"; } return; @@ -534,7 +534,7 @@ GRPCServerImpl::onBackoffFired() // Recovery - FD pressure relieved JLOG(journal_.info()) << "FD pressure relieved - resuming normal operation"; - acceptDelay_ = INITIAL_ACCEPT_DELAY; + backoff_.reset(); for (auto const& ptr : toRepost) { diff --git a/src/xrpld/app/main/GRPCServer.h b/src/xrpld/app/main/GRPCServer.h index 0347f166fa..ece1b8976d 100644 --- a/src/xrpld/app/main/GRPCServer.h +++ b/src/xrpld/app/main/GRPCServer.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -95,9 +96,7 @@ private: // FD throttling and backoff state std::mutex backoffMutex_; bool backoffScheduled_{false}; - std::chrono::milliseconds acceptDelay_{std::chrono::milliseconds{50}}; - static constexpr std::chrono::milliseconds INITIAL_ACCEPT_DELAY{50}; - static constexpr std::chrono::milliseconds MAX_ACCEPT_DELAY{2000}; + ExponentialBackoff backoff_; BackoffTag backoffTag_; grpc::Alarm backoffAlarm_; std::vector> pendingReposts_;