Skip to main content

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}