There are some lifetime concerns wrt scheduler_func
, that are okay in our current codebase, but make the contract perhaps more fragile than it needs to be. Specifically: Logger
has a std::unique_ptr<LogRateLimiter> m_limiter
member. The node’s scheduler holds a reference to the limiter. This means that the limiter may not be destroyed before the scheduler is stopped. In our current code, this is fine, because the logger is a static global, so it can never be destroyed before the node is shutdown (which stops the scheduler).
I ran into this dependency while reviewing / drafting my suggestion on #32604 when I was writing the test code, when I realized that writing an RAII struct (as now e.g. implemented here) for the limiter was not really possible using the std::unique_ptr
approach. I kept it as-is because the non-test code was fine, and I didn’t want to overcomplicate things, but perhaps this follow-up is a good place to at least reconsider this.
Conceptually, I think it makes sense to make m_limiter
a std::shared_ptr
since it has two users: the thing that uses it to consume logs (in our case Logger
), and the thing that uses it to reset stats (in our case CScheduler
). With a shared pointer, we can safely and explicitly manage these dependencies without introducing coupling between the logger and the scheduler.
After playing around with it, it turns out the implementation is actually rather straightforward:
0diff --git a/src/init.cpp b/src/init.cpp
1index 63244c802e..75db37397c 100644
2--- a/src/init.cpp
3+++ b/src/init.cpp
4@@ -1379,7 +1379,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
5 }
6 }, std::chrono::minutes{5});
7
8- LogInstance().SetRateLimiting(std::make_unique<BCLog::LogRateLimiter>(
9+ LogInstance().SetRateLimiting(BCLog::LogRateLimiter::Create(
10 [&scheduler](auto func, auto window) { scheduler.scheduleEvery(std::move(func), window); },
11 BCLog::RATELIMIT_MAX_BYTES,
12 1h));
13diff --git a/src/logging.cpp b/src/logging.cpp
14index e2c27ec54d..3d42bd9bc8 100644
15--- a/src/logging.cpp
16+++ b/src/logging.cpp
17@@ -371,12 +371,21 @@ static size_t MemUsage(const BCLog::Logger::BufferedLog& buflog)
18 memusage::MallocUsage(sizeof(memusage::list_node<BCLog::Logger::BufferedLog>));
19 }
20
21-BCLog::LogRateLimiter::LogRateLimiter(
22- SchedulerFunction scheduler_func,
23- uint64_t max_bytes,
24- std::chrono::seconds reset_window) : m_max_bytes{max_bytes}, m_reset_window{reset_window}
25+BCLog::LogRateLimiter::LogRateLimiter(uint64_t max_bytes, std::chrono::seconds reset_window)
26+ : m_max_bytes{max_bytes}, m_reset_window{reset_window} {}
27+
28+std::shared_ptr<BCLog::LogRateLimiter> BCLog::LogRateLimiter::Create(
29+ SchedulerFunction scheduler_func, uint64_t max_bytes, std::chrono::seconds reset_window)
30 {
31- scheduler_func([this] { Reset(); }, reset_window);
32+ auto limiter{std::shared_ptr<LogRateLimiter>(new LogRateLimiter(max_bytes, reset_window))};
33+ std::weak_ptr<LogRateLimiter> weak_limiter{limiter};
34+ scheduler_func([weak_limiter] {
35+ if (auto shared_limiter{weak_limiter.lock()}) {
36+ shared_limiter->Reset();
37+ }
38+ },
39+ limiter->m_reset_window);
40+ return limiter;
41 }
42
43 BCLog::LogRateLimiter::Status BCLog::LogRateLimiter::Consume(
44diff --git a/src/logging.h b/src/logging.h
45index 5437d73c78..12905f1e47 100644
46--- a/src/logging.h
47+++ b/src/logging.h
48@@ -18,6 +18,7 @@
49 #include <cstring>
50 #include <functional>
51 #include <list>
52+#include <memory>
53 #include <mutex>
54 #include <source_location>
55 #include <string>
56@@ -107,7 +108,7 @@ namespace BCLog {
57 constexpr uint64_t RATELIMIT_MAX_BYTES{1024 * 1024}; // maximum number of bytes per source location that can be logged within the specified time-window
58
59 //! Fixed window rate limiter for logging.
60- class LogRateLimiter
61+ class LogRateLimiter : public std::enable_shared_from_this<LogRateLimiter>
62 {
63 public:
64 //! Keeps track of an individual source location and how many available bytes are left for logging from it.
65@@ -129,6 +130,7 @@ namespace BCLog {
66 std::unordered_map<std::source_location, Stats, SourceLocationHasher, SourceLocationEqual> m_source_locations GUARDED_BY(m_mutex);
67 //! Whether any log locations are suppressed. Cached view on m_source_locations for performance reasons.
68 std::atomic<bool> m_suppression_active{false};
69+ LogRateLimiter(uint64_t max_bytes, std::chrono::seconds reset_window);
70
71 public:
72 using SchedulerFunction = std::function<void(std::function<void()>, std::chrono::milliseconds)>;
73@@ -140,7 +142,10 @@ namespace BCLog {
74 * location.
75 * [@param](/bitcoin-bitcoin/contributor/param/) reset_window Time window after which the stats are reset.
76 */
77- LogRateLimiter(SchedulerFunction scheduler_func, uint64_t max_bytes, std::chrono::seconds reset_window);
78+ static std::shared_ptr<LogRateLimiter> Create(
79+ SchedulerFunction scheduler_func,
80+ uint64_t max_bytes,
81+ std::chrono::seconds reset_window);
82 //! Maximum number of bytes logged per location per window.
83 const uint64_t m_max_bytes;
84 //! Interval after which the window is reset.
85@@ -160,6 +165,8 @@ namespace BCLog {
86 void Reset() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
87 //! Returns true if any log locations are currently being suppressed.
88 bool SuppressionsActive() const { return m_suppression_active; }
89+ //! The function that is used to schedule the reset window.
90+ SchedulerFunction m_scheduler_func;
91 };
92
93 class Logger
94@@ -185,7 +192,7 @@ namespace BCLog {
95 size_t m_buffer_lines_discarded GUARDED_BY(m_cs){0};
96
97 //! Manages the rate limiting of each log location.
98- std::unique_ptr<LogRateLimiter> m_limiter GUARDED_BY(m_cs);
99+ std::shared_ptr<LogRateLimiter> m_limiter GUARDED_BY(m_cs);
100
101 //! Category-specific log level. Overrides `m_log_level`.
102 std::unordered_map<LogFlags, Level> m_category_log_levels GUARDED_BY(m_cs);
103@@ -254,7 +261,7 @@ namespace BCLog {
104 /** Only for testing */
105 void DisconnectTestLogger() EXCLUSIVE_LOCKS_REQUIRED(!m_cs);
106
107- void SetRateLimiting(std::unique_ptr<LogRateLimiter>&& limiter) EXCLUSIVE_LOCKS_REQUIRED(!m_cs)
108+ void SetRateLimiting(std::shared_ptr<LogRateLimiter> limiter) EXCLUSIVE_LOCKS_REQUIRED(!m_cs)
109 {
110 StdLockGuard scoped_lock(m_cs);
111 m_limiter = std::move(limiter);
112diff --git a/src/test/logging_tests.cpp b/src/test/logging_tests.cpp
113index 81c0bacba0..ea0297367d 100644
114--- a/src/test/logging_tests.cpp
115+++ b/src/test/logging_tests.cpp
116@@ -311,7 +311,8 @@ BOOST_AUTO_TEST_CASE(logging_log_rate_limiter)
117 BCLog::LogRateLimiter::SchedulerFunction sched_func{[&scheduler](std::function<void()> func, std::chrono::milliseconds window) {
118 scheduler.scheduleEvery(std::move(func), window);
119 }};
120- BCLog::LogRateLimiter limiter{sched_func, max_bytes, reset_window};
121+ auto limiter_{BCLog::LogRateLimiter::Create(sched_func, max_bytes, reset_window)};
122+ auto& limiter{*limiter_};
123
124 using Status = BCLog::LogRateLimiter::Status;
125 std::source_location source_loc_1{std::source_location::current()};
126@@ -427,8 +428,7 @@ BOOST_FIXTURE_TEST_CASE(logging_filesize_rate_limit, LogSetup)
127 BCLog::LogRateLimiter::SchedulerFunction sched_func{[&scheduler](std::function<void()> func, std::chrono::milliseconds window) {
128 scheduler.scheduleEvery(std::move(func), window);
129 }};
130- std::unique_ptr<BCLog::LogRateLimiter> limiter{std::make_unique<BCLog::LogRateLimiter>(sched_func, bytes_quota, time_window)};
131- LogInstance().SetRateLimiting(std::move(limiter));
132+ LogInstance().SetRateLimiting(BCLog::LogRateLimiter::Create(sched_func, bytes_quota, time_window));
133
134 std::string log_message(line_length, 'a');
135
What do you think?