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