1pub 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 pub fn try_consume(&self, requested: u64) -> u64 {
54 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 #[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 pub fn record(&self, tokens: u64) -> u64 { self.record_capped(tokens, None) }
115
116 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 pub fn refund(&self, tokens: u64) {
153 self.refund_micros(tokens.saturating_mul(MICROS_PER_TOKEN));
154 }
155
156 pub fn refund_micros(&self, micros: u64) {
161 if micros == 0 {
162 return;
163 }
164 self.lock_state().refund(micros);
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub(super) struct Refilled {
172 pub(super) available: u64,
175 pub(super) debt: u64,
177 pub(super) last_refill_nanos: u64,
179 pub(super) fraction: u64,
181}
182
183impl BucketState {
184 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 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 fn quota_exceeded(&self, now: u64) -> bool {
223 self.micro_rate_per_sec != 0 && self.refilled(now).available == 0
224 }
225
226 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 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 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 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 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 struct ManualBucket {
352 bucket: Arc<TokenBucket>,
353 clock: Arc<ManualMonotonicClock>,
354 }
355
356 #[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 #[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 #[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 advance_clock(&clock, Duration::from_millis(500));
513 let g = try_consume_with_timeout(&b, 1024);
514 assert2::assert!(g == 512);
515 }
516
517 #[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 #[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 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 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 #[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 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 #[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 #[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 #[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 check!(debts == vec![1_000, 2_000, 3_000, 4_000]);
820 check!(b.try_consume(1) == 0);
821 }
822
823 #[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 b.refund(160);
833 check!(b.record(0) == 0);
834 check!(b.try_consume(100) == 100);
835
836 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 #[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 #[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 check!(
894 whole_token_request(requested, total, units) == want,
895 "{requested} of {total} at {units}"
896 );
897 }
898
899 #[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 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 #[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 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 #[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 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 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 #[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 #[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 #[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 advance_clock(&clock, Duration::from_millis(2500));
1247 let g = try_consume_with_timeout(&b, 4096);
1248 assert2::assert!(g == 2048);
1249 }
1250}