userver: userver/utils/statistics/recentperiod.hpp Source File
Loading...
Searching...
No Matches
recentperiod.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/statistics/recentperiod.hpp
4/// @brief @copybrief utils::statistics::RecentPeriod
5
6#include <atomic>
7#include <chrono>
8#include <type_traits>
9
10#include <userver/utils/assert.hpp>
11#include <userver/utils/datetime.hpp>
12#include <userver/utils/fixed_array.hpp>
13#include <userver/utils/statistics/fwd.hpp>
14#include <userver/utils/statistics/recentperiod_detail.hpp>
15
16USERVER_NAMESPACE_BEGIN
17
18namespace utils::statistics {
19
20/** \brief Class maintains circular buffer of Counters
21 *
22 * At any time current Counter is accessible for modification via
23 * GetCurrentCounter().
24 * Counter can provide a Reset() member function to clear contents.
25 * @see utils::statistics::Percentile
26 */
27template <typename Counter, typename Result, typename Timer = utils::datetime::SteadyClock>
29public:
30 using Duration = typename Timer::duration;
31
32 static_assert(
33 (detail::ResultWantsAddFunction<Result, Counter, Duration> || detail::ResultCanUseAddAssign<Result, Counter>),
34 "The Result template type argument must provide either Add(Counter, "
35 "Duration, Duration) function or add assignment operator"
36 );
37
38 static constexpr bool kUseAddFunction = detail::ResultWantsAddFunction<Result, Counter, Duration>;
39
40 /**
41 * @param epoch_duration duration of epoch.
42 * @param max_duration max duration to calculate statistics for
43 * must be multiple of epoch_duration.
44 */
45 RecentPeriod(Duration epoch_duration = std::chrono::seconds(5), Duration max_duration = std::chrono::seconds(60))
46 : epoch_duration_(epoch_duration),
47 max_duration_(max_duration),
48 epoch_index_(0),
49 items_(GetSizeForDuration(epoch_duration, max_duration))
50 {
51 UINVARIANT(Duration::zero() < epoch_duration_, "epoch_duration should be greater than 0");
52 }
53
54 Counter& GetCurrentCounter() { return items_[GetCurrentIndex()].counter; }
55
56 Counter& GetPreviousCounter(int epochs_ago) { return items_[GetPreviousIndex(epochs_ago)].counter; }
57
58 /** \brief Aggregates counters within given time range
59 *
60 * @param duration Time range. Special value Duration::min() -> use
61 * whole RecentPeriod range.
62 * @param with_current_epoch Include current (possibly unfinished) counter
63 * into aggregation
64 *
65 * Type Result must have method Add(Counter, Duration, Duration) or allow
66 * addition of counter values
67 */
68 // NOLINTNEXTLINE(readability-const-return-type)
69 const Result GetStatsForPeriod(Duration duration = Duration::min(), bool with_current_epoch = false) const {
70 if (duration == Duration::min()) {
71 duration = max_duration_;
72 }
73
74 Result result{};
75 const Duration now = Timer::now().time_since_epoch();
76 const Duration current_epoch = GetEpochForDuration(now);
77 const Duration start_epoch = current_epoch - duration;
78 const Duration first_epoch_duration = now - current_epoch;
79 std::size_t index = epoch_index_.load();
80
81 for (std::size_t i = 0; i < items_.size(); i++, index = (index + items_.size() - 1) % items_.size()) {
82 const Duration epoch = items_[index].epoch;
83
84 if (epoch > current_epoch) {
85 continue;
86 }
87 if (epoch == current_epoch && !with_current_epoch) {
88 continue;
89 }
90 if (epoch < start_epoch) {
91 break;
92 }
93
94 if constexpr (kUseAddFunction) {
95 const Duration this_epoch_duration = (i == 0) ? first_epoch_duration : epoch_duration_;
96
97 const Duration before_this_epoch_duration = epoch - start_epoch;
98 result.Add(items_[index].counter, this_epoch_duration, before_this_epoch_duration);
99 } else {
100 result += items_[index].counter;
101 }
102 }
103
104 return result;
105 }
106
107 Duration GetEpochDuration() const { return epoch_duration_; }
108
109 Duration GetMaxDuration() const { return max_duration_; }
110
111 void UpdateEpochIfOld() { [[maybe_unused]] auto ignore = GetCurrentIndex(); }
112
113 void Reset() {
114 for (auto& item : items_) {
115 item.Reset(Duration::min());
116 }
117 }
118
119private:
120 size_t GetCurrentIndex() const {
121 while (true) {
122 const Duration now = Timer::now().time_since_epoch();
123 const Duration epoch = GetEpochForDuration(now);
124 std::size_t index = epoch_index_.load();
125 const Duration bucket_epoch = items_[index].epoch.load();
126
127 // Second condition allows non-monotonic timeline (that is common for tests)
128 // but still forbids race (rewrite of fresh bucket by slept after L113 thread)
129 if (epoch > bucket_epoch || epoch + max_duration_ < bucket_epoch) {
130 const std::size_t new_index = (index + 1) % items_.size();
131
132 if (epoch_index_.compare_exchange_weak(index, new_index)) {
133 items_[new_index].epoch = epoch;
134 items_[(new_index + 1) % items_.size()].Reset(epoch + epoch_duration_);
135 return new_index;
136 }
137 } else {
138 return index;
139 }
140 }
141 }
142
143 std::size_t GetPreviousIndex(int epochs_ago) {
144 int index = static_cast<int>(GetCurrentIndex()) - epochs_ago;
145 while (index < 0) {
146 index += items_.size();
147 }
148 return index % items_.size();
149 }
150
151 Duration GetEpochForDuration(Duration duration) const {
152 auto now = std::chrono::duration_cast<Duration>(duration);
153 return now - now % epoch_duration_;
154 }
155
156 static std::size_t GetSizeForDuration(Duration epoch_duration, Duration max_duration) {
157 /* 3 = current bucket, next zero bucket and extra one to handle
158 possible race. */
159 return max_duration.count() / epoch_duration.count() + 3;
160 }
161
162 struct EpochBucket {
163 static constexpr bool kUseReset = detail::CanReset<Counter>;
164 std::atomic<Duration> epoch;
165 Counter counter;
166
167 EpochBucket() { Reset(Duration::min()); }
168
169 void Reset(Duration epoch_duration) {
170 epoch = epoch_duration;
171 if constexpr (kUseReset) {
172 counter.Reset();
173 } else {
174 counter = 0;
175 }
176 }
177 };
178
179 const Duration epoch_duration_;
180 const Duration max_duration_;
181 mutable std::atomic_size_t epoch_index_;
182 mutable utils::FixedArray<EpochBucket> items_;
183};
184
185/// @a Writer support for @a RecentPeriod
186template <typename Counter, typename Result, typename Timer>
187void DumpMetric(Writer& writer, const RecentPeriod<Counter, Result, Timer>& recent_period) {
188 writer = recent_period.GetStatsForPeriod();
189}
190
191/// Reset support for @a RecentPeriod
192template <typename Counter, typename Result, typename Timer>
193void ResetMetric(RecentPeriod<Counter, Result, Timer>& recent_period) {
194 recent_period.Reset();
195}
196
197} // namespace utils::statistics
198
199USERVER_NAMESPACE_END