krabka_throttle/runtime/rate.rs
1//! The rate and burst side of [`TokenBucket`]: the typed setters, the
2//! accessors that read the configuration back, and the reset that publishes
3//! the `{rate, burst, available, fraction, last_refill}` group as one unit.
4//!
5//! The bucket stores micro-tokens, so every dimensioned quantity narrows here.
6//! A byte rate and a byte burst keep their fractional part, so a rate of half
7//! a byte per second is stored as half a byte per second. The byte pair and
8//! the event pair stay separate because a token means a different thing in
9//! each.
10
11use {
12 super::{BucketState, MICROS_PER_TOKEN, TokenBucket},
13 krabka_units::prelude::{
14 ByteRate, ByteRateExt as _, ByteSize, ByteSizeExt as _, Frequency, FrequencyExt as _, Time,
15 secs,
16 },
17 num_traits::ToPrimitive as _,
18 std::sync::atomic::Ordering::Relaxed,
19};
20
21/// The time window that [`TokenBucket::set_byte_rate`] uses for the burst
22/// capacity when the caller does not give one. The burst is the throughput of
23/// this window.
24const DEFAULT_BURST_WINDOW: Time = secs(1);
25
26/// [`MICROS_PER_TOKEN`] as a float, for the fractional conversions.
27const MICROS_PER_TOKEN_F64: f64 = 1_000_000.0;
28
29/// A token count, fractional or not, in micro-tokens, rounded to the nearest.
30///
31/// A negative or `NaN` count is not a quantity, so it becomes `0`, and
32/// anything past `u64::MAX` micro-tokens saturates.
33fn tokens_to_micros(tokens: f64) -> u64 {
34 if tokens.is_nan() || tokens <= 0.0 {
35 return 0;
36 }
37 (tokens * MICROS_PER_TOKEN_F64)
38 .round()
39 .to_u64()
40 .unwrap_or(u64::MAX)
41}
42
43/// A rate in tokens per second, fractional or not, as the bucket's stored
44/// micro-tokens per second.
45///
46/// A rate that is not positive is not a throughput, so it becomes `0`, the
47/// bucket's "no limit configured" sentinel. A positive rate never does: one
48/// under the storage resolution of a micro-token per second is stored as
49/// that resolution, the slowest rate the bucket can meter.
50fn rate_to_micros(tokens_per_sec: f64) -> u64 {
51 if tokens_per_sec.is_nan() || tokens_per_sec <= 0.0 {
52 return 0;
53 }
54 tokens_to_micros(tokens_per_sec).max(1)
55}
56
57/// A stored micro-token count as tokens. Exact below 2^53 micro-tokens.
58fn micros_to_tokens(micros: u64) -> f64 {
59 micros.to_f64().unwrap_or(f64::INFINITY) / MICROS_PER_TOKEN_F64
60}
61
62impl TokenBucket {
63 /// Updates the rate in raw tokens per second, with a one-second burst at
64 /// the new rate. The balance is kept: see
65 /// [`Self::set_token_rate_with_burst`].
66 ///
67 /// This is the primitive. The bucket counts tokens and does not know what a
68 /// token means. Callers that meter a dimensioned quantity should use the
69 /// typed pair that names the dimension: [`Self::set_byte_rate`] or
70 /// [`Self::set_event_rate`].
71 pub fn set_token_rate(&self, tokens_per_sec: u64) {
72 self.set_token_rate_with_burst(tokens_per_sec, tokens_per_sec);
73 }
74
75 /// Updates the rate and the independent burst capacity, both in raw tokens.
76 ///
77 /// A bucket that already has a rate keeps its balance across the change,
78 /// as Kafka's `ClientQuotaManager.updateQuotaMetricConfigs` only swaps a
79 /// metric's bound and leaves what it recorded: the time up to now is
80 /// refilled at the old rate, the balance is capped at the new burst, and
81 /// debt stays debt, so a client in debt is still throttled after a change
82 /// of its quota. A bucket that had no rate has recorded nothing, so it
83 /// starts at `burst`.
84 pub fn set_token_rate_with_burst(&self, new_rate: u64, burst: u64) {
85 self.set_micro_rate_with_burst(
86 new_rate.saturating_mul(MICROS_PER_TOKEN),
87 burst.saturating_mul(MICROS_PER_TOKEN),
88 );
89 }
90
91 /// Updates the rate and the burst, both in micro-tokens, as
92 /// [`Self::set_token_rate_with_burst`] does.
93 ///
94 /// It stores the whole `{rate, burst, available, debt, fraction, last_refill}` group
95 /// in one critical section, so a concurrent [`Self::try_consume`] runs
96 /// either wholly before the reset or wholly after it. No consume can
97 /// commit a balance it computed under the old configuration.
98 fn set_micro_rate_with_burst(&self, micro_rate_per_sec: u64, micro_burst: u64) {
99 let mut state = self.lock_state();
100 let now = self.now_nanos();
101 let (micro_available, micro_debt, micro_refill_fraction) =
102 if state.micro_rate_per_sec == 0 || micro_rate_per_sec == 0 {
103 (micro_burst, 0, 0)
104 } else {
105 let refilled = state.refilled(now);
106 let available = refilled.available.min(micro_burst);
107 let fraction = if available == micro_burst && refilled.debt == 0 {
108 0
109 } else {
110 refilled.fraction
111 };
112 (available, refilled.debt, fraction)
113 };
114 *state = BucketState {
115 micro_rate_per_sec,
116 micro_burst,
117 micro_available,
118 micro_debt,
119 last_refill_nanos: now.max(state.last_refill_nanos),
120 micro_refill_fraction,
121 };
122 // The fast-path rate is published under the lock too, so two
123 // concurrent resets cannot leave it holding the older rate.
124 self.micro_rate_per_sec.store(micro_rate_per_sec, Relaxed);
125 drop(state);
126 }
127
128 /// The configured rate in whole raw tokens per second, rounded down. `0`
129 /// means no limit, or a rate under one token per second:
130 /// [`Self::byte_rate`] reads a fractional rate back exactly.
131 #[must_use]
132 pub fn token_rate(&self) -> u64 { self.fast_path_rate() / MICROS_PER_TOKEN }
133
134 /// The configured burst capacity in whole raw tokens, rounded down. This
135 /// is the most the bucket holds.
136 #[must_use]
137 pub fn token_burst(&self) -> u64 { self.lock_state().micro_burst / MICROS_PER_TOKEN }
138
139 /// Updates a byte throughput and bursts one second's worth, keeping the
140 /// balance as [`Self::set_token_rate_with_burst`] says.
141 ///
142 /// The burst is `rate * DEFAULT_BURST_WINDOW`. `uom` type-checks this as a
143 /// [`ByteRate`] times a [`Time`], which gives a [`ByteSize`].
144 pub fn set_byte_rate(&self, new_rate: ByteRate) {
145 self.set_byte_rate_with_burst(new_rate, (new_rate * DEFAULT_BURST_WINDOW).into());
146 }
147
148 /// Updates a byte throughput and an independent byte burst capacity.
149 ///
150 /// Both keep their fractional part to a millionth of a byte, so a rate of
151 /// half a byte per second is enforced as half a byte per second. Kafka
152 /// holds a byte-rate quota as a double. A positive rate is never stored
153 /// as the unlimited rate `0`. The balance is kept, as
154 /// [`Self::set_token_rate_with_burst`] says.
155 pub fn set_byte_rate_with_burst(&self, new_rate: ByteRate, burst: ByteSize) {
156 self.set_micro_rate_with_burst(
157 rate_to_micros(new_rate.bytes_per_sec_f64()),
158 tokens_to_micros(burst.bytes_f64()),
159 );
160 }
161
162 /// The configured byte throughput, fractional part included.
163 /// [`krabka_units::prelude::ByteRateExt::ZERO`] means no limit.
164 #[must_use]
165 pub fn byte_rate(&self) -> ByteRate {
166 ByteRate::from_bytes_per_sec_f64(micros_to_tokens(self.fast_path_rate()))
167 }
168
169 /// Whether the bucket already runs at `rate`, as
170 /// [`Self::set_byte_rate_with_burst`] would store it.
171 ///
172 /// A caller that re-applies a configured rate compares with this rather
173 /// than with [`Self::byte_rate`]: a rate finer than a micro-token per
174 /// second reads back rounded, so a comparison that never matched would
175 /// reset the bucket on every re-apply.
176 #[must_use]
177 pub fn runs_at_byte_rate(&self, rate: ByteRate) -> bool {
178 self.fast_path_rate() == rate_to_micros(rate.bytes_per_sec_f64())
179 }
180
181 /// The configured byte burst capacity, fractional part included.
182 #[must_use]
183 pub fn byte_burst(&self) -> ByteSize {
184 ByteSize::from_bytes_f64(micros_to_tokens(self.lock_state().micro_burst))
185 }
186
187 /// Updates an event throughput, such as samples, records, or requests, and
188 /// bursts one second's worth.
189 ///
190 /// A token here is one event, not one byte. If you meter events with the
191 /// byte pair above, the code compiles but the result is wrong. This is why
192 /// the two pairs are separate.
193 pub fn set_event_rate(&self, new_rate: Frequency) {
194 let per_sec = new_rate.per_sec_u64();
195 self.set_token_rate_with_burst(per_sec, per_sec);
196 }
197
198 /// Updates an event throughput and an independent burst, in whole events.
199 pub fn set_event_rate_with_burst(&self, new_rate: Frequency, burst: u64) {
200 self.set_token_rate_with_burst(new_rate.per_sec_u64(), burst);
201 }
202
203 /// The configured event throughput in whole events per second, rounded
204 /// down. Zero means no limit.
205 #[must_use]
206 pub fn event_rate(&self) -> Frequency { Frequency::from_per_sec_u64(self.token_rate()) }
207}
208
209#[cfg(test)]
210mod tests {
211 use {
212 super::*,
213 assert2::check,
214 krabka_units::prelude::{bytes, bytes_per_sec, kibibytes, kibibytes_per_sec, mebibytes},
215 };
216
217 // The bucket narrows every rate and burst to raw bytes and bytes-per-second
218 // for the verified integer kernel, so the accessors are the only place a
219 // scale factor could go missing. Each case pairs a value written in one unit
220 // with the byte-denominated magnitude it must read back as: a dropped or
221 // doubled 1024 in either direction fails here.
222 #[yare::parameterized(
223 zero = { bytes_per_sec(0), bytes(0) },
224 one = { bytes_per_sec(1), bytes(1) },
225 one_kibibyte_rate = { kibibytes_per_sec(1), bytes(1024) },
226 one_kibibyte_burst = { bytes_per_sec(1024), kibibytes(1) },
227 decimal_rate = { bytes_per_sec(3_000_000), mebibytes(2) },
228 kibibyte_rate = { kibibytes_per_sec(64), mebibytes(1) },
229 fractional = { ByteRate::from_bytes_per_sec_f64(0.5), ByteSize::from_bytes_f64(5.5) },
230 quarter_rate = { ByteRate::from_bytes_per_sec_f64(1.25), ByteSize::from_bytes_f64(0.75) },
231 submicro = {
232 ByteRate::from_bytes_per_sec_f64(0.000_001),
233 ByteSize::from_bytes_f64(0.000_011),
234 },
235 )]
236 fn rate_and_burst_round_trip_through_the_accessors(rate: ByteRate, burst: ByteSize) {
237 let b = TokenBucket::new();
238 b.set_byte_rate_with_burst(rate, burst);
239 check!((b.byte_rate(), b.byte_burst()) == (rate, burst));
240 }
241
242 /// The event pair exists separately from the byte pair because a token
243 /// means a different thing in each -- the API's own warning is that mixing
244 /// them "compiles but the result is wrong". Only the byte pair was round
245 /// tripped, so nothing checked that an event rate reads back as one, nor
246 /// that the raw token primitive underneath both publishes what it is given.
247 #[yare::parameterized(
248 zero = { 0, 0 },
249 one = { 1, 1 },
250 ten_to_hundred = { 10, 100 },
251 thousand_to_two_fifty = { 1_000, 250 },
252 )]
253 fn event_and_token_rates_round_trip_through_the_accessors(per_sec: u64, burst: u64) {
254 // (rate per second, independent burst in whole events)
255 let rate = Frequency::from_per_sec_u64(per_sec);
256
257 let b = TokenBucket::new();
258 b.set_event_rate_with_burst(rate, burst);
259 check!((b.event_rate(), b.token_burst()) == (rate, burst));
260
261 // `set_event_rate` bursts one second of the rate, as the byte pair does.
262 let b = TokenBucket::new();
263 b.set_event_rate(rate);
264 check!((b.event_rate(), b.token_burst()) == (rate, per_sec));
265
266 // The untyped primitive both pairs delegate to.
267 let b = TokenBucket::new();
268 b.set_token_rate(per_sec);
269 check!((b.token_rate(), b.token_burst()) == (per_sec, per_sec));
270 }
271
272 /// A positive byte rate never reads back as the unlimited rate `0`:
273 /// one under the storage resolution is stored as that resolution, and a
274 /// rate that is not positive is no limit.
275 #[yare::parameterized(
276 tiny_positive = { 1.0e-12, ByteRate::from_bytes_per_sec_f64(0.000_001) },
277 below_resolution = { 0.000_000_4, ByteRate::from_bytes_per_sec_f64(0.000_001) },
278 zero = { 0.0, ByteRate::from_bytes_per_sec(0) },
279 negative = { -3.0, ByteRate::from_bytes_per_sec(0) },
280 not_a_number = { f64::NAN, ByteRate::from_bytes_per_sec(0) },
281 )]
282 fn a_positive_byte_rate_is_never_unlimited(rate: f64, want: ByteRate) {
283 let b = TokenBucket::new();
284 b.set_byte_rate_with_burst(ByteRate::from_bytes_per_sec_f64(rate), bytes(1));
285 check!(b.byte_rate() == want, "{rate}");
286 }
287
288 /// A rate finer than the storage resolution still matches the bucket it
289 /// configured, and a different rate does not.
290 #[yare::parameterized(
291 exact = { 0.123_456_7, true },
292 rounds_same = { 0.123_457, true },
293 rounds_lower = { 0.123_456, false },
294 different = { 0.5, false },
295 zero = { 0.0, false },
296 )]
297 fn runs_at_byte_rate_compares_the_stored_rate(rate: f64, want: bool) {
298 let b = TokenBucket::new();
299 b.set_byte_rate_with_burst(ByteRate::from_bytes_per_sec_f64(0.123_456_7), bytes(1));
300 check!(
301 b.runs_at_byte_rate(ByteRate::from_bytes_per_sec_f64(rate)) == want,
302 "{rate}"
303 );
304 }
305
306 // `set_rate` derives the burst from one second's worth of the rate, so the
307 // burst it publishes must be the byte count the rate delivers in that
308 // second — not the rate's bare number in some other unit.
309 #[test]
310 fn set_rate_bursts_one_second_of_throughput() {
311 let b = TokenBucket::new();
312 b.set_byte_rate(kibibytes_per_sec(64));
313 check!((b.byte_rate(), b.byte_burst()) == (kibibytes_per_sec(64), kibibytes(64)));
314 }
315}