Skip to main content

krabka_throttle/runtime/
consume.rs

1//! [`TokenBucket::try_consume`]: turn the time elapsed since the last refill
2//! into micro-tokens, cap them at the burst, and grant from the result, all in
3//! one critical section. [`TokenBucket::record`] charges the same bucket
4//! without a grant limit and leaves what the balance could not cover as debt.
5//!
6//! The arithmetic that caps and grants is the verified [`plan_consume`]
7//! kernel, run over micro-token counts. [`BucketState::consume`] and
8//! [`BucketState::record`] are the whole step on the locked group, so they can
9//! be tested without threads.
10
11/// The micro-token request a whole-token consume hands to [`plan_consume`].
12///
13/// `requested` is in whole tokens and `total` is the capped balance in
14/// storage units, `units_per_token` to a token. The result is the request in
15/// storage units, cut down to the whole tokens `total` holds, so the grant
16/// [`plan_consume`] makes from it is a whole number of tokens and a part
17/// token stays in the bucket.
18///
19/// Production passes [`MICROS_PER_TOKEN`]. The stateright model in
20/// `tests/bucket_model.rs` drives this same function with a small
21/// `units_per_token`, which keeps its search small.
22///
23/// # Panics
24/// Panics if `units_per_token` is zero.
25pub use krabka_verified::quota::quota_whole_request as whole_token_request;
26use {
27    super::{BucketState, MICROS_PER_TOKEN, TokenBucket},
28    krabka_units::prelude::{Time, TimeExt as _},
29    krabka_verified::{
30        quota::{quota_charge, quota_credit, quota_debt_cap, quota_refill},
31        throttle::{AvailableTokens, BurstCapacity, RefillTokens, RequestedTokens, plan_consume},
32    },
33    std::time::Duration,
34};
35
36impl TokenBucket {
37    /// Tries to consume up to `requested` whole tokens.
38    ///
39    /// This method returns the whole tokens actually granted. A part token in
40    /// the bucket is not granted; it stays for a later consume. A bucket in
41    /// debt from [`Self::record`] grants nothing. Rate-0 grants the full
42    /// request.
43    ///
44    /// The refill, the grant, and the new balance are one critical section on
45    /// the bucket's group, so a concurrent
46    /// [`Self::set_token_rate_with_burst`] runs wholly before or wholly after
47    /// this call, and concurrent consumes never lose each other's refill. See
48    /// the module documentation of the runtime for the design.
49    ///
50    /// # Panics
51    /// Panics if the injected clock reads more than `u64::MAX` nanoseconds
52    /// since its origin, about 584 years.
53    pub fn try_consume(&self, requested: u64) -> u64 {
54        // Unthrottled fast path: a `0` here orders this consume before any
55        // reset still in flight, so it may grant without the lock.
56        if self.fast_path_rate() == 0 {
57            return requested;
58        }
59        let mut state = self.lock_state();
60        let now = self.now_nanos();
61        state
62            .consume(now, requested)
63            .map_or(requested, |micros| micros / MICROS_PER_TOKEN)
64    }
65
66    /// Whether the bucket has nothing left to grant: its balance, refilled up
67    /// to now, is spent or in debt. A rate-0 bucket has no limit and is never
68    /// exceeded.
69    ///
70    /// This is Kafka's `ReplicationQuotaManager.isQuotaExceeded`, which a
71    /// replication throttle asks before it reads rather than after. While it
72    /// answers `false` the read goes out whole, and the caller then charges
73    /// its size with [`Self::record_bounded`], past the balance if it must.
74    /// The overshoot is debt, and the debt makes the next ask answer `true`
75    /// until the refill has repaid it. So the long-term rate holds, and a
76    /// batch larger than the whole burst still moves.
77    ///
78    /// # Panics
79    /// Panics if the injected clock reads more than `u64::MAX` nanoseconds
80    /// since its origin, about 584 years.
81    #[must_use]
82    pub fn quota_exceeded(&self) -> bool {
83        if self.fast_path_rate() == 0 {
84            return false;
85        }
86        let state = self.lock_state();
87        let now = self.now_nanos();
88        state.quota_exceeded(now)
89    }
90
91    /// Charges `tokens` whole tokens whatever the bucket holds, and returns
92    /// the debt the charge leaves, in micro-tokens, [`MICROS_PER_TOKEN`] to a
93    /// token. The debt is `0` while the charge fits the balance.
94    ///
95    /// This is Kafka's `Sensor.record(value, timeMs, true)` on a quota's
96    /// `Rate`: the value is recorded first and the quota is checked after, so
97    /// an over-quota request is charged in full. The part the balance could
98    /// not cover is debt, and the refill repays it before it adds to the
99    /// balance, so a client that keeps sending above its quota is throttled by
100    /// everything it has sent above the bound and not only by its latest
101    /// request. A caller turns the debt into the throttle time `debt / rate`,
102    /// which is Kafka's `QuotaUtils.throttleTime`.
103    ///
104    /// A rate-0 bucket has no limit, records nothing and reports no debt.
105    ///
106    /// The debt is not capped, which is right for a byte-rate client quota:
107    /// its throttle is exactly `debt / rate`. A quota whose throttle is
108    /// bounded, or whose debt only ever starves a consumer of the bucket,
109    /// uses [`Self::record_bounded`].
110    ///
111    /// # Panics
112    /// Panics if the injected clock reads more than `u64::MAX` nanoseconds
113    /// since its origin, about 584 years.
114    pub fn record(&self, tokens: u64) -> u64 { self.record_capped(tokens, None) }
115
116    /// Charges `tokens` as [`Self::record`] does, and keeps at most `max_wait`
117    /// of debt: the debt the refill repays in `max_wait` at the current rate.
118    ///
119    /// Kafka's quota sample window forgets what a client recorded about
120    /// `quota.window.num * quota.window.size.seconds` ago (`SampledStat` purges
121    /// an expired sample), so the debt of an overload does not outlast the
122    /// window. A debt that grows without a bound outlasts it: a client whose
123    /// throttle is capped at `max_wait` would be throttled at the cap on every
124    /// request until the whole uncapped debt was repaid, and a bucket that
125    /// grants nothing while in debt would starve its other consumers for as
126    /// long. The cap bounds the throttle derived from the debt by the wait
127    /// converted to clock nanoseconds. It rounds credit down to complete
128    /// micro-tokens, so fractional credit cannot extend debt beyond that wait.
129    ///
130    /// # Panics
131    /// Panics if the injected clock reads more than `u64::MAX` nanoseconds
132    /// since its origin, about 584 years.
133    pub fn record_bounded(&self, tokens: u64, max_wait: Time) -> u64 {
134        self.record_capped(tokens, Some(max_wait))
135    }
136
137    fn record_capped(&self, tokens: u64, max_wait: Option<Time>) -> u64 {
138        if self.fast_path_rate() == 0 {
139            return 0;
140        }
141        let mut state = self.lock_state();
142        let now = self.now_nanos();
143        state.record(now, tokens, max_wait).unwrap_or(0)
144    }
145
146    /// Gives back `tokens` that an earlier charge took and the caller did not
147    /// use, capped at the burst.
148    ///
149    /// This is Kafka's `ClientQuotaManager.unrecordQuotaSensor`: a fetch that
150    /// is throttled sends no records, so the bytes it was charged come off
151    /// the quota again. A rate-0 bucket holds no balance to give back to.
152    pub fn refund(&self, tokens: u64) {
153        self.refund_micros(tokens.saturating_mul(MICROS_PER_TOKEN));
154    }
155
156    /// Gives back `micros` micro-tokens that an earlier charge took, capped at
157    /// the burst. Debt is repaid first. Refunding the effective micro-token
158    /// charge restores the refilled balance if no debt was discarded by
159    /// saturation or a cap and no other operation changed the bucket in between.
160    pub fn refund_micros(&self, micros: u64) {
161        if micros == 0 {
162            return;
163        }
164        self.lock_state().refund(micros);
165    }
166}
167
168/// What the time up to a clock reading makes of a bucket: the balance after
169/// the refill, and the elapsed time the refill accounts for.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub(super) struct Refilled {
172    /// Micro-tokens the bucket can grant, refill added and capped at the
173    /// burst.
174    pub(super) available: u64,
175    /// Debt the refill did not repay.
176    pub(super) debt: u64,
177    /// The clock reading through which elapsed time has been credited.
178    pub(super) last_refill_nanos: u64,
179    /// Fractional micro-token numerator, with denominator one billion.
180    pub(super) fraction: u64,
181}
182
183impl BucketState {
184    /// Turns the time elapsed up to `now` into micro-tokens: the refill
185    /// repays the debt first, then adds to the balance, capped at the burst.
186    ///
187    /// Claim the entire elapsed interval and keep its fractional micro-token
188    /// numerator separately. Repeating a clock reading earns no more tokens,
189    /// and short intervals accumulate without losing their fractional credit.
190    ///
191    /// The bucket must have a rate. Nothing is stored: the caller stores the
192    /// result together with the rest of its step.
193    pub(super) fn refilled(&self, now: u64) -> Refilled {
194        let (available, debt, fraction) = quota_refill(
195            self.micro_available,
196            self.micro_debt,
197            self.micro_refill_fraction,
198            now.saturating_sub(self.last_refill_nanos),
199            self.micro_rate_per_sec,
200            self.micro_burst,
201        );
202        Refilled {
203            available,
204            debt,
205            last_refill_nanos: now.max(self.last_refill_nanos),
206            fraction,
207        }
208    }
209
210    /// Stores the balance and fraction while claiming the refilled interval.
211    fn store(&mut self, refilled: Refilled, new_available: u64, debt: u64) {
212        self.last_refill_nanos = refilled.last_refill_nanos;
213        self.micro_refill_fraction = refilled.fraction;
214        self.micro_available = new_available;
215        self.micro_debt = debt;
216    }
217
218    /// Whether the balance, refilled up to `now`, has nothing to grant. A
219    /// state with no rate has no quota: the lock-free rate read in
220    /// [`TokenBucket::quota_exceeded`] can see a rate that a concurrent reset
221    /// has already cleared, so the locked rate decides.
222    fn quota_exceeded(&self, now: u64) -> bool {
223        self.micro_rate_per_sec != 0 && self.refilled(now).available == 0
224    }
225
226    /// Refills the bucket for the time elapsed up to `now`, then grants up to
227    /// `requested` whole tokens from it, and returns the grant in
228    /// micro-tokens, or `None` when the bucket has no limit.
229    ///
230    /// Every value is computed before the first store, so a panic cannot leave
231    /// the group half updated.
232    fn consume(&mut self, now: u64, requested: u64) -> Option<u64> {
233        if self.micro_rate_per_sec == 0 {
234            return None;
235        }
236        let refilled = self.refilled(now);
237        let (grant, new_available) = plan_consume(
238            AvailableTokens(refilled.available),
239            RefillTokens(0),
240            BurstCapacity(self.micro_burst),
241            RequestedTokens(whole_token_request(
242                requested,
243                refilled.available,
244                MICROS_PER_TOKEN,
245            )),
246        );
247        self.store(refilled, new_available.0, refilled.debt);
248        Some(grant.0)
249    }
250
251    /// Refills the bucket for the time elapsed up to `now`, then charges
252    /// `tokens` whole tokens: the balance pays what it can, and the rest is
253    /// debt, at most what the refill repays in `max_wait` when there is one.
254    /// Returns the debt after the charge in micro-tokens, or `None` when the
255    /// bucket has no limit.
256    fn record(&mut self, now: u64, tokens: u64, max_wait: Option<Time>) -> Option<u64> {
257        if self.micro_rate_per_sec == 0 {
258            return None;
259        }
260        let refilled = self.refilled(now);
261        let request = tokens.saturating_mul(MICROS_PER_TOKEN);
262        let (new_available, debt) = quota_charge(
263            refilled.available,
264            refilled.debt,
265            request,
266            self.micro_burst,
267            self.debt_cap(max_wait),
268        );
269        self.store(refilled, new_available, debt);
270        Some(debt)
271    }
272
273    /// The most debt, in micro-tokens, that the refill repays in `max_wait`,
274    /// or no cap without one.
275    fn debt_cap(&self, max_wait: Option<Time>) -> u64 {
276        max_wait.map_or(u64::MAX, |wait| {
277            let Ok(wait) = Duration::try_from_secs_f64(wait.secs_f64().max(0.0)) else {
278                return u64::MAX;
279            };
280            quota_debt_cap(wait.as_nanos(), self.micro_rate_per_sec)
281        })
282    }
283
284    /// Adds `micros` back to the balance, capped at the burst, after it has
285    /// repaid the debt. A rate-0 bucket is unthrottled and keeps no balance,
286    /// so it is left alone.
287    fn refund(&mut self, micros: u64) {
288        if self.micro_rate_per_sec != 0 {
289            (self.micro_available, self.micro_debt) = quota_credit(
290                self.micro_available,
291                self.micro_debt,
292                micros,
293                self.micro_burst,
294            );
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use {
302        super::*,
303        assert2::check,
304        krabka_units::prelude::{
305            ByteRate, ByteRateExt as _, ByteSize, ByteSizeExt as _, bytes, bytes_per_sec, secs,
306        },
307        qubit_clock::ManualMonotonicClock,
308        std::{
309            iter,
310            sync::{
311                Arc,
312                atomic::{AtomicBool, Ordering::Relaxed},
313                mpsc::{self, RecvTimeoutError},
314            },
315            thread,
316            time::Duration,
317        },
318    };
319
320    const SEC: u64 = 1_000_000_000;
321    const M: u64 = MICROS_PER_TOKEN;
322    const UNLIMITED_BUCKET_STATE: BucketState = BucketState {
323        micro_rate_per_sec: 0,
324        micro_burst: 0,
325        micro_available: 0,
326        micro_debt: 0,
327        last_refill_nanos: 0,
328        micro_refill_fraction: 0,
329    };
330
331    /// Builds a bucket whose refill clock is a manual timeline starting at its
332    /// own zero-duration origin, which is what the refill differences measure
333    /// from.
334    ///
335    /// The function returns the bucket with the [`ManualMonotonicClock`]
336    /// handle, so the test can advance logical time with `clock.advance(..)`
337    /// instead of sleeping.
338    fn manual_bucket() -> ManualBucket {
339        let clock = ManualMonotonicClock::new_shared();
340        let bucket = Arc::new(TokenBucket::with_clock(Arc::<ManualMonotonicClock>::clone(
341            &clock,
342        )));
343        ManualBucket { bucket, clock }
344    }
345
346    fn advance_clock(clock: &ManualMonotonicClock, duration: Duration) {
347        clock.advance(duration).expect("manual time moves forward");
348    }
349
350    /// A bucket and the manual clock that drives its refill.
351    struct ManualBucket {
352        bucket: Arc<TokenBucket>,
353        clock: Arc<ManualMonotonicClock>,
354    }
355
356    /// Kafka's `isQuotaExceeded`: a bucket with any balance is within its
357    /// quota, however small the balance and however large the read it is
358    /// about to let through. A spent balance or debt is over the quota until
359    /// the refill repays it, and a bucket with no rate has no quota.
360    #[yare::parameterized(
361        full = { 1_000, 0, 0, false },
362        partly_drawn = { 1_000, 999, 0, false },
363        spent = { 1_000, 1_000, 0, true },
364        spent_then_one_millisecond = { 1_000, 1_000, 1, false },
365        in_debt = { 1_000, 3_000, 0, true },
366        in_debt_until_repaid = { 1_000, 3_000, 2_000, true },
367        repaid = { 1_000, 3_000, 2_001, false },
368        unlimited = { 0, 3_000, 0, false },
369    )]
370    fn quota_exceeded_reports_a_spent_or_indebted_balance(
371        rate: u32,
372        recorded: u64,
373        later_ms: u64,
374        exceeded: bool,
375    ) {
376        let ManualBucket { bucket, clock } = manual_bucket();
377        bucket.set_byte_rate(bytes_per_sec(rate));
378        bucket.record(recorded);
379        advance_clock(&clock, Duration::from_millis(later_ms));
380
381        check!(bucket.quota_exceeded() == exceeded);
382    }
383
384    /// A reset that clears the rate between the lock-free rate read and the
385    /// lock leaves a state with no rate and no balance. That is no quota, not
386    /// an exceeded one.
387    #[test]
388    fn a_state_without_a_rate_is_never_exceeded() {
389        check!(!UNLIMITED_BUCKET_STATE.quota_exceeded(SEC));
390    }
391
392    fn bucket_state(
393        rate: u64,
394        burst: u64,
395        available: u64,
396        debt: u64,
397        last_refill: u64,
398    ) -> BucketState {
399        BucketState {
400            micro_rate_per_sec: rate,
401            micro_burst: burst,
402            micro_available: available,
403            micro_debt: debt,
404            last_refill_nanos: last_refill,
405            micro_refill_fraction: 0,
406        }
407    }
408
409    fn assert_consume_step_matches(
410        actual: (Option<u64>, BucketState),
411        expected: (Option<u64>, BucketState),
412    ) {
413        check!(actual == expected);
414    }
415
416    fn assert_rate_change_grant(actual: u64, expected: u64) {
417        check!(actual == expected);
418    }
419
420    fn advance_one_ns_and_poll(bucket: &TokenBucket, clock: &ManualMonotonicClock) {
421        clock.advance(Duration::from_nanos(1)).unwrap();
422        check!(bucket.try_consume(0) == 0);
423    }
424
425    const TRY_CONSUME_TIMEOUT: Duration = Duration::from_secs(2);
426
427    fn try_consume_with_timeout(bucket: &Arc<TokenBucket>, requested: u64) -> u64 {
428        let bucket = Arc::clone(bucket);
429        let (tx, rx) = mpsc::channel();
430        let handle = thread::spawn(move || {
431            let granted = bucket.try_consume(requested);
432            let _ = tx.send(granted);
433        });
434
435        match rx.recv_timeout(TRY_CONSUME_TIMEOUT) {
436            Ok(granted) => {
437                handle.join().expect("try_consume worker panicked");
438                granted
439            }
440            Err(RecvTimeoutError::Timeout) => {
441                drop(handle);
442                panic!("try_consume({requested}) did not complete within {TRY_CONSUME_TIMEOUT:?}");
443            }
444            Err(RecvTimeoutError::Disconnected) => {
445                handle.join().expect("try_consume worker panicked");
446                panic!("try_consume worker exited without sending a result");
447            }
448        }
449    }
450
451    #[test]
452    fn zero_rate_grants_full_request() {
453        let b = TokenBucket::new();
454        assert2::assert!(b.try_consume(1024) == 1024);
455    }
456
457    #[test]
458    fn first_consume_under_rate_succeeds() {
459        let b = Arc::new(TokenBucket::new());
460        b.set_byte_rate(bytes_per_sec(1024));
461        assert2::assert!(try_consume_with_timeout(&b, 512) == 512);
462    }
463
464    #[test]
465    fn independent_burst_can_exceed_rate() {
466        let b = Arc::new(TokenBucket::new());
467        b.set_byte_rate_with_burst(bytes_per_sec(100), bytes(1000));
468        check!(
469            (
470                b.byte_rate(),
471                b.byte_burst(),
472                try_consume_with_timeout(&b, 500)
473            ) == (bytes_per_sec(100), bytes(1000), 500)
474        );
475    }
476
477    /// A refund gives back what a consume took, and never more than the
478    /// burst: consuming 600 of a 1000-token burst and refunding it leaves the
479    /// whole burst; a second refund cannot push past it.
480    #[test]
481    fn refund_returns_granted_tokens_up_to_the_burst() {
482        let ManualBucket {
483            bucket: b,
484            clock: _clock,
485        } = manual_bucket();
486        b.set_byte_rate_with_burst(bytes_per_sec(100), bytes(1000));
487        let granted = try_consume_with_timeout(&b, 600);
488        b.refund(granted);
489        let after_one_refund = try_consume_with_timeout(&b, 1000);
490        b.refund(after_one_refund);
491        b.refund(500);
492        let after_overflowing_refund = try_consume_with_timeout(&b, 2000);
493        check!((granted, after_one_refund, after_overflowing_refund) == (600, 1000, 1000));
494    }
495
496    #[test]
497    fn consume_drains_bucket() {
498        let b = Arc::new(TokenBucket::new());
499        b.set_byte_rate(bytes_per_sec(1024));
500        assert2::assert!(try_consume_with_timeout(&b, 1024) == 1024);
501        let g = try_consume_with_timeout(&b, 1024);
502        assert2::assert!(g < 100);
503    }
504
505    #[test]
506    fn bucket_refills_at_rate_after_elapsed_time() {
507        let ManualBucket { bucket: b, clock } = manual_bucket();
508        b.set_byte_rate(bytes_per_sec(1024));
509        try_consume_with_timeout(&b, 1024);
510        // 500ms at 1024 tokens/s refills exactly 512 tokens — deterministic,
511        // where a real 500ms sleep only gets "roughly" 512 under scheduler jitter.
512        advance_clock(&clock, Duration::from_millis(500));
513        let g = try_consume_with_timeout(&b, 1024);
514        assert2::assert!(g == 512);
515    }
516
517    /// A bucket polled faster than one token per interval still refills at its
518    /// rate: the part of an interval that did not make a whole token carries
519    /// over to the next call instead of being dropped.
520    #[test]
521    fn frequent_empty_polls_keep_the_partial_refill() {
522        let ManualBucket { bucket: b, clock } = manual_bucket();
523        b.set_token_rate_with_burst(1, 1);
524        check!(try_consume_with_timeout(&b, 1) == 1);
525
526        let mut grants = Vec::new();
527        for _ in 0..10 {
528            advance_clock(&clock, Duration::from_millis(100));
529            grants.push(try_consume_with_timeout(&b, 1));
530        }
531
532        assert2::assert!(grants.as_slice() == &[0, 0, 0, 0, 0, 0, 0, 0, 0, 1][..]);
533    }
534
535    /// One `record` step on the locked group: the charge is taken in full, the
536    /// balance pays what it can, and the debt is what it could not. Each row
537    /// pins the debt reported and the whole group after the step.
538    #[yare::parameterized(
539        unlimited = { UNLIMITED_BUCKET_STATE, 5 * SEC, 7, None, UNLIMITED_BUCKET_STATE },
540        within_balance = {
541            bucket_state(10 * M, 20 * M, 20 * M, 0, 0),
542            0,
543            12,
544            Some(0),
545            bucket_state(10 * M, 20 * M, 8 * M, 0, 0),
546        },
547        charge_past_balance = {
548            bucket_state(10 * M, 20 * M, 5 * M, 0, 0),
549            0,
550            12,
551            Some(7 * M),
552            bucket_state(10 * M, 20 * M, 0, 7 * M, 0),
553        },
554        adds_to_existing_debt = {
555            bucket_state(10 * M, 20 * M, 0, 7 * M, 0),
556            0,
557            3,
558            Some(10 * M),
559            bucket_state(10 * M, 20 * M, 0, 10 * M, 0),
560        },
561        refill_repaid_before_charge = {
562            bucket_state(10 * M, 20 * M, 0, 7 * M, 0),
563            SEC,
564            4,
565            Some(M),
566            bucket_state(10 * M, 20 * M, 0, M, SEC),
567        },
568        fractional_refill_remainder = {
569            bucket_state(4, 10 * M, 0, 0, 0),
570            SEC / 2 + SEC / 8,
571            10,
572            Some(10 * M - 2),
573            BucketState {
574                micro_refill_fraction: 500_000_000,
575                ..bucket_state(4, 10 * M, 0, 10 * M - 2, SEC / 2 + SEC / 8)
576            },
577        },
578        half_token_debt = {
579            bucket_state(M / 2, M, M / 2, 0, 0),
580            0,
581            1,
582            Some(M / 2),
583            bucket_state(M / 2, M, 0, M / 2, 0),
584        },
585        debt_saturates = {
586            bucket_state(M, M, 0, u64::MAX - 5, 0),
587            0,
588            1,
589            Some(u64::MAX),
590            bucket_state(M, M, 0, u64::MAX, 0),
591        },
592    )]
593    fn record_step_charges_in_full_and_leaves_the_rest_as_debt(
594        before: BucketState,
595        now: u64,
596        tokens: u64,
597        debt: Option<u64>,
598        after: BucketState,
599    ) {
600        let mut state = before;
601        let reported = state.record(now, tokens, None);
602        check!((reported, state) == (debt, after));
603    }
604
605    #[yare::parameterized(
606        charge_from_balance = { 5 * M, 0, None, 12, (5 * M, 0) },
607        charge_while_in_debt = { 0, 7 * M, None, 12, (0, 7 * M) },
608        charge_after_wait = { 5 * M, 0, Some(secs(2)), 12, (10 * M, 0) },
609        saturated_debt = { 0, u64::MAX, None, 1, (0, u64::MAX - M) },
610    )]
611    fn charge_refund_round_trip_excludes_forgotten_debt(
612        available: u64,
613        debt: u64,
614        wait: Option<Time>,
615        charge: u64,
616        expected: (u64, u64),
617    ) {
618        const M: u64 = MICROS_PER_TOKEN;
619        let mut state = BucketState {
620            micro_rate_per_sec: M,
621            micro_burst: 20 * M,
622            micro_available: available,
623            micro_debt: debt,
624            last_refill_nanos: 0,
625            micro_refill_fraction: 0,
626        };
627        let _ = state.record(0, charge, wait);
628        state.refund(charge * M);
629        check!((state.micro_available, state.micro_debt) == expected);
630    }
631
632    #[test]
633    fn refill_polling_matches_an_independent_elapsed_time_ledger() {
634        for rate in [1, 3, 1_500_000_000, u64::MAX] {
635            for (available, debt, burst) in [
636                (0, 0, 10 * MICROS_PER_TOKEN),
637                (0, 7 * MICROS_PER_TOKEN, 10 * MICROS_PER_TOKEN),
638                (0, u64::MAX, u64::MAX),
639            ] {
640                let bucket = TokenBucket::new();
641                let mut state = *bucket.lock_state();
642                state.micro_rate_per_sec = rate;
643                state.micro_burst = burst;
644                state.micro_available = available;
645                state.micro_debt = debt;
646                state.last_refill_nanos = 0;
647                let mut latest = 0;
648                for now in [0, 1, 1, 2, 1, 3, 333_333_334, 1_000_000_000, u64::MAX] {
649                    latest = latest.max(now);
650                    let earned =
651                        i128::try_from(u128::from(latest) * u128::from(rate) / 1_000_000_000)
652                            .expect("a u64 product divided by one billion fits i128");
653                    let expected =
654                        (i128::from(available) - i128::from(debt) + earned).min(i128::from(burst));
655                    check!(state.consume(now, 0) == Some(0));
656                    check!(
657                        i128::from(state.micro_available) - i128::from(state.micro_debt)
658                            == expected,
659                        "rate={rate}, now={now}, initial debt={debt}"
660                    );
661                }
662            }
663        }
664    }
665
666    #[test]
667    fn rate_changes_preserve_fractional_credit_and_full_buckets_discard_it() {
668        let ManualBucket { bucket, clock } = manual_bucket();
669        bucket.set_token_rate_with_burst(1_500, 10);
670        check!(bucket.try_consume(10) == 10);
671        advance_one_ns_and_poll(&bucket, &clock);
672        check!(bucket.lock_state().micro_refill_fraction == 500_000_000);
673        bucket.set_token_rate_with_burst(500, 10);
674        advance_one_ns_and_poll(&bucket, &clock);
675        check!(bucket.lock_state().micro_available == 2);
676        check!(bucket.lock_state().micro_refill_fraction == 0);
677
678        // A rate-0 transition starts a new unlimited generation.
679        bucket.set_token_rate_with_burst(0, 10);
680        bucket.set_token_rate_with_burst(1_500, 10);
681        clock.advance(Duration::from_nanos(1)).unwrap();
682        // Refill while full, then empty it at this same clock reading.
683        check!(bucket.try_consume(10) == 10);
684        check!(bucket.lock_state().micro_refill_fraction == 0);
685        advance_one_ns_and_poll(&bucket, &clock);
686        check!(bucket.lock_state().micro_available == 1);
687        check!(bucket.lock_state().micro_refill_fraction == 500_000_000);
688    }
689
690    #[test]
691    fn consuming_traces_keep_clock_credit_after_spending_and_repaying_debt() {
692        const M: u64 = MICROS_PER_TOKEN;
693        for (debt, expected) in [(0, 4 * M), (3 * M, 2 * M)] {
694            let bucket = TokenBucket::new();
695            let mut state = *bucket.lock_state();
696            state.micro_rate_per_sec = 1_000 * M;
697            state.micro_burst = 2 * M;
698            state.micro_debt = debt;
699            state.last_refill_nanos = 0;
700            let mut granted = 0;
701            for now in [0, 3_000_000, 3_000_000, 5_000_000, 4_000_000] {
702                granted += state.consume(now, 10).unwrap();
703            }
704            check!(
705                (
706                    granted,
707                    state.micro_available,
708                    state.micro_debt,
709                    state.micro_refill_fraction,
710                    state.last_refill_nanos
711                ) == (expected, 0, 0, 0, 5_000_000)
712            );
713        }
714        let bucket = TokenBucket::new();
715        let mut state = *bucket.lock_state();
716        state.micro_rate_per_sec = 1_500 * M;
717        state.micro_burst = 10 * M;
718        state.last_refill_nanos = 0;
719        let mut granted = 0;
720        for now in [1, 666_667, 666_667, 1_333_334] {
721            granted += state.consume(now, 1).unwrap();
722        }
723        check!(
724            (
725                granted,
726                state.micro_available,
727                state.micro_debt,
728                state.micro_refill_fraction
729            ) == (2 * M, 1, 0, 0)
730        );
731    }
732
733    /// A bounded charge keeps at most what the refill repays in `max_wait`, so
734    /// an overload that ends is forgotten after that wait, as a sample that
735    /// leaves Kafka's quota window is, instead of throttling the bucket for
736    /// its whole size. Each row charges a thousand seconds of a `1_000` token/s
737    /// bucket, then reads the debt and what the bucket grants three seconds
738    /// later.
739    #[yare::parameterized(
740        unbounded = { None, 999_000, 0 },
741        two_second_bound = { Some(secs(2)), 2_000, 1_000 },
742        zero_bound = { Some(secs(0)), 0, 1_000 },
743    )]
744    fn bounded_debt_is_repaid_within_the_wait(max_wait: Option<Time>, debt: u64, grant: u64) {
745        // (label, max wait, debt after the charge in tokens, grant after 3 s)
746        let ManualBucket { bucket: b, clock } = manual_bucket();
747        b.set_token_rate_with_burst(1_000, 1_000);
748
749        let reported = b.record_capped(1_000_000, max_wait) / MICROS_PER_TOKEN;
750        advance_clock(&clock, Duration::from_secs(3));
751        let granted = b.try_consume(u64::MAX);
752
753        check!((reported, granted) == (debt, grant));
754    }
755
756    #[yare::parameterized(
757        submicro_rate_rounds_to_zero = { 0.000_001, 0 },
758        three_micro_rate_rounds_to_one = { 0.000_003, 1 },
759    )]
760    fn bounded_fractional_debt_is_repaid_at_the_exact_wait(rate: f64, cap: u64) {
761        let ManualBucket { bucket, clock } = manual_bucket();
762        bucket.set_byte_rate_with_burst(ByteRate::from_bytes_per_sec_f64(rate), bytes(1));
763        check!(bucket.record_bounded(u64::MAX, Time::from_secs_f64(0.5)) == cap);
764        advance_clock(&clock, Duration::from_millis(500));
765        check!(bucket.record(0) == 0);
766    }
767
768    /// The bound follows the rate the bucket has when it is charged: a rate
769    /// change keeps the balance, and the next bounded charge trims the debt to
770    /// the new rate's wait.
771    #[test]
772    fn the_bound_follows_the_current_rate() {
773        let ManualBucket {
774            bucket: b,
775            clock: _clock,
776        } = manual_bucket();
777        b.set_token_rate_with_burst(1_000, 1_000);
778        check!(b.record_bounded(10_000, secs(5)) == 5_000 * MICROS_PER_TOKEN);
779
780        b.set_token_rate_with_burst(100, 100);
781
782        check!(b.record_bounded(0, secs(5)) == 500 * MICROS_PER_TOKEN);
783    }
784
785    /// A bucket at half a token per second admits half a token per second:
786    /// a caller asking for one token every second is granted one every other
787    /// second, and a charge in between owes the half token it lacks.
788    #[test]
789    fn a_fractional_rate_is_enforced_at_its_rate() {
790        let ManualBucket { bucket: b, clock } = manual_bucket();
791        b.set_byte_rate_with_burst(ByteRate::from_bytes_per_sec_f64(0.5), bytes(1));
792        let mut whole = Vec::new();
793        for _ in 0..5 {
794            whole.push(try_consume_with_timeout(&b, 1));
795            advance_clock(&clock, Duration::from_secs(1));
796        }
797        let debt = b.record(1);
798
799        check!((whole, debt) == (vec![1, 0, 1, 0, 1], MICROS_PER_TOKEN / 2));
800    }
801
802    /// The overage of a charge is kept, not credited back: a bucket driven at
803    /// twice its rate by charges of a constant size owes more with every
804    /// charge, and a consume finds it empty however long the last mute was.
805    /// Before #1212 the bucket granted only what it held, so the refill of
806    /// the mute paid the overage and the client sent about twice its quota.
807    #[test]
808    fn charges_above_the_rate_accumulate_as_debt() {
809        let ManualBucket { bucket: b, clock } = manual_bucket();
810        b.set_token_rate_with_burst(1_000, 1_000);
811        let mut debts = Vec::new();
812        for _ in 0..4 {
813            debts.push(b.record(2_000) / MICROS_PER_TOKEN);
814            advance_clock(&clock, Duration::from_secs(1));
815        }
816
817        // The first charge is 1000 over the balance. Each later second's
818        // refill repays 1000 of the debt, and the next charge adds 2000.
819        check!(debts == vec![1_000, 2_000, 3_000, 4_000]);
820        check!(b.try_consume(1) == 0);
821    }
822
823    /// A debt is repaid by the refill and by a refund, and both come before
824    /// the balance.
825    #[test]
826    fn refill_and_refund_repay_the_debt_before_the_balance() {
827        let ManualBucket { bucket: b, clock } = manual_bucket();
828        b.set_token_rate_with_burst(100, 100);
829        check!(b.record(160) == 60 * MICROS_PER_TOKEN);
830
831        // Refunding the whole charge gives back the balance it started from.
832        b.refund(160);
833        check!(b.record(0) == 0);
834        check!(b.try_consume(100) == 100);
835
836        // A second's refill of 100 repays a debt of 30 and leaves 70.
837        check!(b.record(30) == 30 * MICROS_PER_TOKEN);
838        advance_clock(&clock, Duration::from_secs(1));
839        check!(b.try_consume(1_000) == 70);
840    }
841
842    /// A charge on an unlimited bucket records nothing, whatever its size.
843    #[test]
844    fn an_unlimited_bucket_records_no_debt() {
845        let b = TokenBucket::new();
846        check!((b.record(u64::MAX), b.try_consume(5)) == (0, 5));
847    }
848
849    /// A part-token charge given back is available again, capped at the
850    /// burst, as a whole-token refund is.
851    #[test]
852    fn refund_micros_returns_a_part_token() {
853        let ManualBucket {
854            bucket: b,
855            clock: _clock,
856        } = manual_bucket();
857        b.set_byte_rate_with_burst(
858            ByteRate::from_bytes_per_sec_f64(0.25),
859            ByteSize::from_bytes_f64(0.75),
860        );
861        let debt = b.record(1);
862        b.refund_micros(MICROS_PER_TOKEN);
863        let after_refund = b.record(0);
864        b.refund_micros(u64::MAX);
865        let capped = b.record(5);
866
867        check!(
868            (debt, after_refund, capped)
869                == (
870                    MICROS_PER_TOKEN / 4,
871                    0,
872                    5 * MICROS_PER_TOKEN - MICROS_PER_TOKEN * 3 / 4
873                )
874        );
875    }
876
877    #[yare::parameterized(
878        zero_request = { 0, 5, 2, 0 },
879        less_than_one_token = { 1, 1, 2, 0 },
880        one_token_and_remainder = { 1, 3, 2, 2 },
881        caps_at_total = { 3, 5, 2, 4 },
882        one_token_requested = { 1, 5, 2, 2 },
883        unbounded_request = { u64::MAX, 7, 1, 7 },
884        microtoken_storage = { u64::MAX, 2_500_000, MICROS_PER_TOKEN, 2_000_000 },
885    )]
886    fn whole_token_request_cuts_to_the_whole_tokens_held(
887        requested: u64,
888        total: u64,
889        units: u64,
890        want: u64,
891    ) {
892        // (requested tokens, total units, units per token, request in units)
893        check!(
894            whole_token_request(requested, total, units) == want,
895            "{requested} of {total} at {units}"
896        );
897    }
898
899    /// A consume that arrives while a reset holds the group waits for it and
900    /// then grants against the new configuration, never against a value it
901    /// read before the reset.
902    #[test]
903    fn try_consume_waits_for_an_in_flight_reset() {
904        let ManualBucket { bucket: b, clock } = manual_bucket();
905        b.set_token_rate_with_burst(10, 10);
906        advance_clock(&clock, Duration::from_secs(1));
907
908        // Stand in for a reset part-way through its critical section: the lock
909        // is held and the group is rewritten to a smaller burst.
910        let mut in_flight_reset = b.lock_state();
911        *in_flight_reset = BucketState {
912            micro_rate_per_sec: 10 * MICROS_PER_TOKEN,
913            micro_burst: 3 * MICROS_PER_TOKEN,
914            micro_available: 3 * MICROS_PER_TOKEN,
915            micro_debt: 0,
916            last_refill_nanos: 1_000_000_000,
917            micro_refill_fraction: 0,
918        };
919
920        let (tx, rx) = mpsc::channel();
921        let worker_bucket = Arc::clone(&b);
922        let handle = thread::spawn(move || {
923            let _ = tx.send(worker_bucket.try_consume(10));
924        });
925        match rx.recv_timeout(Duration::from_millis(50)) {
926            Err(RecvTimeoutError::Timeout) => {}
927            Ok(granted) => panic!("try_consume granted {granted} during a reset"),
928            Err(RecvTimeoutError::Disconnected) => {
929                panic!("try_consume worker exited during a reset")
930            }
931        }
932
933        drop(in_flight_reset);
934        let granted = rx
935            .recv_timeout(TRY_CONSUME_TIMEOUT)
936            .expect("try_consume completes once the reset releases the group");
937        handle.join().expect("try_consume worker panicked");
938        check!(granted == 3);
939    }
940
941    /// Consumers racing each other never lose a refill one of them claimed.
942    ///
943    /// The bucket starts empty, and the manual clock advances by exactly one
944    /// token's worth of time per step while the consumers poll. The burst is
945    /// larger than the whole refill, so no token is capped away. Every refilled
946    /// token must therefore end up granted: a consume that claimed time and
947    /// then dropped its tokens would leave the total short.
948    #[test]
949    fn concurrent_consumers_never_lose_a_claimed_refill() {
950        const STEPS: u64 = 2_000;
951        const RATE: u64 = 1_000;
952        let ManualBucket { bucket: b, clock } = manual_bucket();
953        b.set_token_rate_with_burst(RATE, STEPS + 1);
954        check!(try_consume_with_timeout(&b, STEPS + 1) == STEPS + 1);
955
956        let stop = Arc::new(AtomicBool::new(false));
957        let mut consumers: Vec<_> = iter::repeat_with(|| {
958            let b = Arc::clone(&b);
959            let stop = Arc::clone(&stop);
960            thread::spawn(move || {
961                let mut granted = 0;
962                while !stop.load(Relaxed) {
963                    granted += b.try_consume(1);
964                }
965                granted
966            })
967        })
968        .take(3)
969        .collect();
970
971        for _ in 0..STEPS {
972            advance_clock(&clock, Duration::from_millis(1));
973            thread::yield_now();
974        }
975        stop.store(true, Relaxed);
976        // Every consumer runs before the clock moves, so they are all spawned
977        // first and joined only here.
978        let raced: u64 = consumers
979            .drain(..)
980            .map(|h| h.join().expect("consumer panicked"))
981            .sum();
982        let leftover = try_consume_with_timeout(&b, u64::MAX);
983
984        check!(raced + leftover == STEPS);
985    }
986
987    // Stress the reset path: consumers racing a stream of resets that shrink
988    // and grow the burst must never see `available` above the current burst.
989    // A reset straddled by a consume's read and commit would let that commit
990    // store a balance computed under the old, larger burst. The real clock
991    // runs, so consumes refill between resets, which the straddle needs.
992    #[test]
993    fn concurrent_set_rate_never_over_grants_past_burst() {
994        struct RateBurst {
995            rate: u64,
996            burst: u64,
997        }
998        const fn rate_burst(rate: u64, burst: u64) -> RateBurst { RateBurst { rate, burst } }
999        // Rate and burst pairs the resetter cycles through, largest burst first.
1000        const CONFIGS: [RateBurst; 4] = [
1001            rate_burst(1_000_000, 4_096),
1002            rate_burst(1_000_000, 1_024),
1003            rate_burst(3_000_000, 128),
1004            rate_burst(500_000, 3),
1005        ];
1006        const MAX_BURST: u64 = CONFIGS[0].burst;
1007        let b = Arc::new(TokenBucket::new());
1008        b.set_token_rate_with_burst(CONFIGS[0].rate, CONFIGS[0].burst);
1009        let stop = Arc::new(AtomicBool::new(false));
1010
1011        let resetter = {
1012            let b = Arc::clone(&b);
1013            let stop = Arc::clone(&stop);
1014            thread::spawn(move || {
1015                for RateBurst { rate, burst } in CONFIGS.iter().cycle() {
1016                    if stop.load(Relaxed) {
1017                        break;
1018                    }
1019                    b.set_token_rate_with_burst(*rate, *burst);
1020                    thread::yield_now();
1021                }
1022            })
1023        };
1024
1025        // The group's own invariant, sampled under its lock throughout.
1026        let observer = {
1027            let b = Arc::clone(&b);
1028            let stop = Arc::clone(&stop);
1029            thread::spawn(move || {
1030                let mut over_burst = None;
1031                while !stop.load(Relaxed) && over_burst.is_none() {
1032                    let state = *b.lock_state();
1033                    if state.micro_available > state.micro_burst {
1034                        over_burst = Some(state);
1035                    }
1036                }
1037                over_burst
1038            })
1039        };
1040
1041        let (done_tx, done_rx) = mpsc::channel();
1042        let mut consumer_handles = Vec::new();
1043        for requested in [0, 1, 128] {
1044            let b = Arc::clone(&b);
1045            let done_tx = done_tx.clone();
1046            consumer_handles.push(thread::spawn(move || {
1047                for _ in 0..5_000 {
1048                    let g = b.try_consume(requested);
1049                    if g > requested.min(MAX_BURST) {
1050                        let _ = done_tx.send(Err(g));
1051                        return;
1052                    }
1053                }
1054                let _ = done_tx.send(Ok(()));
1055            }));
1056        }
1057        drop(done_tx);
1058
1059        let mut over_grant = None;
1060        let mut timed_out = false;
1061        for _ in 0..consumer_handles.len() {
1062            match done_rx.recv_timeout(Duration::from_secs(5)) {
1063                Ok(Ok(())) => {}
1064                Ok(Err(g)) => {
1065                    over_grant = Some(g);
1066                    break;
1067                }
1068                Err(RecvTimeoutError::Timeout) => {
1069                    timed_out = true;
1070                    break;
1071                }
1072                Err(RecvTimeoutError::Disconnected) => break,
1073            }
1074        }
1075
1076        stop.store(true, Relaxed);
1077        resetter.join().expect("resetter panicked");
1078        let over_burst = observer.join().expect("observer panicked");
1079        for h in consumer_handles {
1080            h.join().expect("consumer panicked");
1081        }
1082
1083        check!(let None = over_grant, "a grant exceeded the request or the burst");
1084        check!(let None = over_burst, "available exceeded the burst");
1085        check!(!timed_out);
1086        let state = *b.lock_state();
1087        check!(state.micro_available <= state.micro_burst);
1088    }
1089
1090    #[yare::parameterized(
1091        positive_rate_zero_burst = { 1024, 0, 1, 0 },
1092    )]
1093    fn positive_rate_zero_burst_grants_zero(rate: u32, burst: u32, requested: u64, expected: u64) {
1094        let b = Arc::new(TokenBucket::new());
1095        b.set_byte_rate_with_burst(bytes_per_sec(rate), bytes(burst));
1096
1097        assert2::assert!(try_consume_with_timeout(&b, requested) == expected);
1098    }
1099
1100    /// A bucket in debt stays in debt across a change of its rate: the client
1101    /// is still throttled, now by `debt / new rate`.
1102    #[test]
1103    fn a_rate_change_keeps_the_debt() {
1104        let ManualBucket {
1105            bucket: b,
1106            clock: _clock,
1107        } = manual_bucket();
1108        b.set_token_rate_with_burst(1_000, 1_000);
1109        check!(b.record(3_000) == 2_000 * MICROS_PER_TOKEN);
1110
1111        b.set_token_rate_with_burst(2_000, 2_000);
1112
1113        check!((b.try_consume(1), b.record(0)) == (0, 2_000 * MICROS_PER_TOKEN));
1114    }
1115
1116    /// A rate change keeps the balance, as Kafka's
1117    /// `ClientQuotaManager.updateQuotaMetricConfigs` swaps a metric's bound
1118    /// and leaves what it recorded (#1241). Each row drives one bucket
1119    /// through a consume, an optional wait, a change of rate and burst, and a
1120    /// consume that asks for more than the bucket can hold. The last grant is
1121    /// the balance that survived the change.
1122    #[yare::parameterized(
1123        drained_rate_increase = { 1_000, 1_000, 1_000, 0, 2_000, 2_000, 0 },
1124        old_rate_refill = { 1_000, 1_000, 1_000, 500, 2_000, 2_000, 500 },
1125        capped_at_new_burst = { 1_000, 1_000, 0, 0, 2_000, 300, 300 },
1126        keeps_balance_under_new_burst = { 1_000, 1_000, 600, 0, 2_000, 2_000, 400 },
1127        first_rate_starts_full = { 0, 0, 0, 0, 1_000, 1_000, 1_000 },
1128        unlimited_then_limited = { 0, 0, 5_000, 0, 1_000, 700, 700 },
1129    )]
1130    fn a_rate_change_keeps_the_balance(
1131        rate: u64,
1132        burst: u64,
1133        spent: u64,
1134        wait_ms: u64,
1135        new_rate: u64,
1136        new_burst: u64,
1137        want: u64,
1138    ) {
1139        let ManualBucket { bucket: b, clock } = manual_bucket();
1140        b.set_token_rate_with_burst(rate, burst);
1141        b.try_consume(spent);
1142        advance_clock(&clock, Duration::from_millis(wait_ms));
1143        b.set_token_rate_with_burst(new_rate, new_burst);
1144        assert_rate_change_grant(b.try_consume(u64::MAX), want);
1145    }
1146
1147    /// One whole-token consume step on the locked group, table-driven over the
1148    /// cases that differ only in the group, the clock and the request. Each
1149    /// row pins the grant and the whole group after the step. The group is in
1150    /// micro-tokens and the request in whole tokens.
1151    #[yare::parameterized(
1152        unlimited = { UNLIMITED_BUCKET_STATE, 5 * SEC, 7, None, UNLIMITED_BUCKET_STATE },
1153        full_elapsed_second = {
1154            bucket_state(10 * M, 20 * M, 0, 0, 0),
1155            SEC,
1156            4,
1157            Some(4 * M),
1158            bucket_state(10 * M, 20 * M, 6 * M, 0, SEC),
1159        },
1160        refill_capped_at_burst = {
1161            bucket_state(10 * M, 20 * M, 15 * M, 0, 0),
1162            SEC,
1163            0,
1164            Some(0),
1165            bucket_state(10 * M, 20 * M, 20 * M, 0, SEC),
1166        },
1167        fractional_remainder = {
1168            bucket_state(4, 10 * M, 0, 0, 0),
1169            SEC / 2 + SEC / 8,
1170            10,
1171            Some(0),
1172            BucketState {
1173                micro_refill_fraction: 500_000_000,
1174                ..bucket_state(4, 10 * M, 2, 0, SEC / 2 + SEC / 8)
1175            },
1176        },
1177        zero_burst = {
1178            bucket_state(10 * M, 0, 0, 0, 0),
1179            SEC,
1180            3,
1181            Some(0),
1182            bucket_state(10 * M, 0, 0, 0, SEC),
1183        },
1184        clock_behind_last_refill = {
1185            bucket_state(10 * M, 20 * M, 3 * M, 0, SEC),
1186            0,
1187            5,
1188            Some(3 * M),
1189            bucket_state(10 * M, 20 * M, 0, 0, SEC),
1190        },
1191        half_token_stays_in_bucket = {
1192            bucket_state(M / 2, M, 0, 0, 0),
1193            SEC,
1194            1,
1195            Some(0),
1196            bucket_state(M / 2, M, M / 2, 0, SEC),
1197        },
1198        two_half_tokens_make_one = {
1199            bucket_state(M / 2, M, 0, 0, 0),
1200            2 * SEC,
1201            1,
1202            Some(M),
1203            bucket_state(M / 2, M, 0, 0, 2 * SEC),
1204        },
1205        part_balance_grants_whole_tokens = {
1206            bucket_state(M, 3 * M, 2 * M + M / 4, 0, 0),
1207            0,
1208            5,
1209            Some(2 * M),
1210            bucket_state(M, 3 * M, M / 4, 0, 0),
1211        },
1212        refill_reduces_debt = {
1213            bucket_state(10 * M, 20 * M, 0, 8 * M, 0),
1214            SEC / 2,
1215            3,
1216            Some(0),
1217            bucket_state(10 * M, 20 * M, 0, 3 * M, SEC / 2),
1218        },
1219        refill_past_debt_goes_to_balance = {
1220            bucket_state(10 * M, 20 * M, 0, 4 * M, 0),
1221            SEC,
1222            3,
1223            Some(3 * M),
1224            bucket_state(10 * M, 20 * M, 3 * M, 0, SEC),
1225        },
1226    )]
1227    fn consume_step_refills_caps_and_grants(
1228        before: BucketState,
1229        now: u64,
1230        requested: u64,
1231        grant: Option<u64>,
1232        after: BucketState,
1233    ) {
1234        let mut state = before;
1235        let granted = state.consume(now, requested);
1236        assert_consume_step_matches((granted, state), (grant, after));
1237    }
1238
1239    #[test]
1240    fn bucket_caps_at_burst_capacity() {
1241        let ManualBucket { bucket: b, clock } = manual_bucket();
1242        b.set_byte_rate_with_burst(bytes_per_sec(1024), bytes(2048));
1243        try_consume_with_timeout(&b, 2048);
1244        // 2.5s at 1024 tokens/s would refill 2560 tokens, but the 2048 burst cap
1245        // clamps it; advancing logical time makes this exact and instant.
1246        advance_clock(&clock, Duration::from_millis(2500));
1247        let g = try_consume_with_timeout(&b, 4096);
1248        assert2::assert!(g == 2048);
1249    }
1250}