1use std::fmt;
15use std::pin::Pin;
16use std::sync::Arc;
17use std::sync::Mutex;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::task::{Context, Poll};
20use std::time::{Duration, Instant};
21
22use axum::response::{IntoResponse, Response};
23use bytes::Bytes;
24use futures_util::Stream;
25use http::{HeaderValue, StatusCode};
26use objectstore_service::id::ObjectContext;
27use objectstore_types::scope::Scopes;
28use objectstore_types::usecase::Usecase;
29use serde::{Deserialize, Serialize};
30use thiserror::Error;
31
32pub const HEADER_ENVOY_RATELIMITED: &str = "x-envoy-ratelimited";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
47pub enum LimitExceeded {
48 #[error("service at capacity")]
50 Capacity,
51 #[error("rate limit exceeded")]
53 Allowance,
54}
55
56impl LimitExceeded {
57 pub fn status(self) -> StatusCode {
59 match self {
60 Self::Capacity => StatusCode::SERVICE_UNAVAILABLE,
61 Self::Allowance => StatusCode::TOO_MANY_REQUESTS,
62 }
63 }
64}
65
66impl IntoResponse for LimitExceeded {
67 fn into_response(self) -> Response {
68 let mut response = (self.status(), self.to_string()).into_response();
69 response
70 .headers_mut()
71 .insert(HEADER_ENVOY_RATELIMITED, HeaderValue::from_static("true"));
72 response
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum RateLimitRejection {
79 BandwidthGlobal,
81 BandwidthUsecase,
83 BandwidthScope,
85 ThroughputGlobal,
87 ThroughputUsecase,
89 ThroughputScope,
91 ThroughputRule,
93}
94
95impl RateLimitRejection {
96 fn error(self) -> LimitExceeded {
98 match self {
99 Self::ThroughputRule => LimitExceeded::Allowance,
100 Self::BandwidthGlobal
101 | Self::BandwidthUsecase
102 | Self::BandwidthScope
103 | Self::ThroughputGlobal
104 | Self::ThroughputUsecase
105 | Self::ThroughputScope => LimitExceeded::Capacity,
106 }
107 }
108
109 pub fn as_str(self) -> &'static str {
111 match self {
112 RateLimitRejection::BandwidthGlobal => "bandwidth_global",
113 RateLimitRejection::BandwidthUsecase => "bandwidth_usecase",
114 RateLimitRejection::BandwidthScope => "bandwidth_scope",
115 RateLimitRejection::ThroughputGlobal => "throughput_global",
116 RateLimitRejection::ThroughputUsecase => "throughput_usecase",
117 RateLimitRejection::ThroughputScope => "throughput_scope",
118 RateLimitRejection::ThroughputRule => "throughput_rule",
119 }
120 }
121}
122
123impl fmt::Display for RateLimitRejection {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.write_str(self.as_str())
126 }
127}
128
129#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
131pub struct RateLimits {
132 pub throughput: ThroughputLimits,
134 pub bandwidth: BandwidthLimits,
136}
137
138#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
145pub struct ThroughputLimits {
146 pub global_rps: Option<u32>,
150
151 pub burst: u32,
156
157 pub usecase_pct: Option<u8>,
161
162 pub scope_pct: Option<u8>,
170
171 pub rules: Vec<ThroughputRule>,
173}
174
175#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
183pub struct ThroughputRule {
184 pub usecase: Option<Usecase>,
188
189 pub scopes: Vec<(String, String)>,
194
195 pub rps: Option<u32>,
200
201 pub pct: Option<u8>,
206}
207
208impl ThroughputRule {
209 pub fn matches(&self, context: &ObjectContext) -> bool {
211 if let Some(ref rule_usecase) = self.usecase
212 && rule_usecase != &context.usecase
213 {
214 return false;
215 }
216
217 for (scope_name, scope_value) in &self.scopes {
218 match context.scopes.get_value(scope_name) {
219 Some(value) if value == scope_value => (),
220 _ => return false,
221 }
222 }
223
224 true
225 }
226}
227
228#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
235pub struct BandwidthLimits {
236 pub global_bps: Option<u64>,
240
241 #[serde(default = "default_burst_ms")]
246 pub burst_ms: u64,
247
248 pub usecase_pct: Option<u8>,
252
253 pub scope_pct: Option<u8>,
257
258 #[serde(default)]
263 pub report_only: bool,
264}
265
266fn default_burst_ms() -> u64 {
267 1000
268}
269
270impl Default for BandwidthLimits {
271 fn default() -> Self {
272 Self {
273 global_bps: None,
274 burst_ms: default_burst_ms(),
275 usecase_pct: None,
276 scope_pct: None,
277 report_only: false,
278 }
279 }
280}
281
282#[derive(Debug)]
291pub struct RateLimiter {
292 bandwidth: BandwidthRateLimiter,
293 throughput: ThroughputRateLimiter,
294}
295
296impl RateLimiter {
297 pub fn new(config: RateLimits) -> Self {
301 Self {
302 bandwidth: BandwidthRateLimiter::new(config.bandwidth),
303 throughput: ThroughputRateLimiter::new(config.throughput),
304 }
305 }
306
307 pub fn start(&self) {
311 self.throughput.start();
312 }
313
314 pub fn check(&self, context: &ObjectContext, key: Option<&str>) -> Result<(), LimitExceeded> {
321 let rejection = self
325 .bandwidth
326 .check(context)
327 .or_else(|| self.throughput.check(context));
328
329 let Some(rejection) = rejection else {
330 return Ok(());
331 };
332
333 objectstore_metrics::count!(
334 "server.request.rate_limited",
335 reason = rejection.as_str(),
336 usecase = context.usecase.to_string()
337 );
338 objectstore_log::warn!(
339 reason = rejection.as_str(),
340 usecase = %context.usecase,
341 scopes = %context.scopes.as_api_path(),
342 key,
343 "Request rejected: rate limit exceeded"
344 );
345 Err(rejection.error())
346 }
347
348 pub fn bandwidth_handle(&self, context: &ObjectContext) -> BandwidthHandle {
352 self.bandwidth.handle(context)
353 }
354
355 pub fn record_bandwidth(&self, context: &ObjectContext, bytes: u64) {
360 self.bandwidth.handle(context).record(bytes);
361 objectstore_metrics::count!("server.bandwidth.bytes" += bytes);
362 }
363
364 pub fn bandwidth_limit(&self) -> Option<u64> {
366 self.bandwidth.config.global_bps
367 }
368
369 pub fn throughput_limit(&self) -> Option<u32> {
371 self.throughput.config.global_rps
372 }
373
374 pub fn bandwidth_total_bytes(&self) -> u64 {
376 self.bandwidth.total_bytes.load(Ordering::Relaxed)
377 }
378
379 pub fn throughput_total_admitted(&self) -> u64 {
381 self.throughput.total_admitted.load(Ordering::Relaxed)
382 }
383}
384
385#[derive(Debug)]
392struct BandwidthBucket {
393 tat: AtomicU64,
397 nanos_per_byte: f64,
399 burst_ns: u64,
401}
402
403impl BandwidthBucket {
404 fn new(bps: u64, burst_ms: u64) -> Self {
406 let nanos_per_byte = 1_000_000_000.0 / bps as f64;
407 let burst_ns = burst_ms * 1_000_000;
408 Self {
409 tat: AtomicU64::new(0),
410 nanos_per_byte,
411 burst_ns,
412 }
413 }
414
415 fn spend(&self, now_nanos: u64, bytes: u64) {
417 let weight = (bytes as f64 * self.nanos_per_byte) as u64;
418 self.tat
421 .update(Ordering::Relaxed, Ordering::Relaxed, |old| {
422 old.max(now_nanos).saturating_add(weight)
423 });
424 }
425
426 fn check(&self, now_nanos: u64) -> bool {
428 self.tat.load(Ordering::Relaxed) <= now_nanos.saturating_add(self.burst_ns)
429 }
430}
431
432fn pct_of_u32(value: u32, pct: u8) -> u32 {
434 let scaled = u64::from(value) * u64::from(pct) / 100;
435 u32::try_from(scaled).unwrap_or(u32::MAX)
436}
437
438fn pct_of_u64(value: u64, pct: u8) -> u64 {
440 let scaled = u128::from(value) * u128::from(pct) / 100;
441 u64::try_from(scaled).unwrap_or(u64::MAX)
442}
443
444#[derive(Debug)]
445struct BandwidthRateLimiter {
446 config: BandwidthLimits,
447 global: Option<Arc<BandwidthBucket>>,
449 epoch: Instant,
451 total_bytes: Arc<AtomicU64>,
453 usecases: Arc<papaya::HashMap<Usecase, Arc<BandwidthBucket>>>,
456 scopes: Arc<papaya::HashMap<Scopes, Arc<BandwidthBucket>>>,
457}
458
459impl BandwidthRateLimiter {
460 fn new(config: BandwidthLimits) -> Self {
461 let global = config
462 .global_bps
463 .map(|bps| Arc::new(BandwidthBucket::new(bps, config.burst_ms)));
464
465 if let Some(limit) = config.global_bps {
466 objectstore_metrics::gauge!("server.bandwidth.limit" = limit);
467 }
468
469 Self {
470 global,
471 epoch: Instant::now(),
472 total_bytes: Arc::new(AtomicU64::new(0)),
473 usecases: Arc::new(papaya::HashMap::new()),
474 scopes: Arc::new(papaya::HashMap::new()),
475 config,
476 }
477 }
478
479 fn now_nanos(&self) -> u64 {
481 self.epoch.elapsed().as_nanos() as u64
483 }
484
485 fn check(&self, context: &ObjectContext) -> Option<RateLimitRejection> {
489 if self.config.report_only {
490 return None;
491 }
492
493 let now_nanos = self.now_nanos();
494
495 if let Some(ref global) = self.global
497 && !global.check(now_nanos)
498 {
499 return Some(RateLimitRejection::BandwidthGlobal);
500 }
501
502 if self.usecase_bps().is_some() {
504 let guard = self.usecases.pin();
505 if let Some(bucket) = guard.get(&context.usecase)
506 && !bucket.check(now_nanos)
507 {
508 return Some(RateLimitRejection::BandwidthUsecase);
509 }
510 }
511
512 if self.scope_bps().is_some() {
514 let guard = self.scopes.pin();
515 if let Some(bucket) = guard.get(&context.scopes)
516 && !bucket.check(now_nanos)
517 {
518 return Some(RateLimitRejection::BandwidthScope);
519 }
520 }
521
522 None
523 }
524
525 fn handle(&self, context: &ObjectContext) -> BandwidthHandle {
527 let mut buckets = Vec::new();
528
529 if let Some(ref global) = self.global {
530 buckets.push(Arc::clone(global));
531 }
532
533 if let Some(usecase_bps) = self.usecase_bps() {
534 let guard = self.usecases.pin();
535 let bucket = guard.get_or_insert_with(context.usecase.clone(), || {
536 Arc::new(BandwidthBucket::new(usecase_bps, self.config.burst_ms))
537 });
538 buckets.push(Arc::clone(bucket));
539 }
540
541 if let Some(scope_bps) = self.scope_bps() {
542 let guard = self.scopes.pin();
543 let bucket = guard.get_or_insert_with(context.scopes.clone(), || {
544 Arc::new(BandwidthBucket::new(scope_bps, self.config.burst_ms))
545 });
546 buckets.push(Arc::clone(bucket));
547 }
548
549 objectstore_metrics::gauge!(
550 "server.rate_limiter.bandwidth.usecase_map_size" = self.usecases.len()
551 );
552 objectstore_metrics::gauge!(
553 "server.rate_limiter.bandwidth.scope_map_size" = self.scopes.len()
554 );
555
556 BandwidthHandle {
557 buckets,
558 total_bytes: Arc::clone(&self.total_bytes),
559 epoch: self.epoch,
560 }
561 }
562
563 fn usecase_bps(&self) -> Option<u64> {
565 let global_bps = self.config.global_bps?;
566 let pct = self.config.usecase_pct?;
567 Some(pct_of_u64(global_bps, pct))
568 }
569
570 fn scope_bps(&self) -> Option<u64> {
572 let global_bps = self.config.global_bps?;
573 let pct = self.config.scope_pct?;
574 Some(pct_of_u64(global_bps, pct))
575 }
576}
577
578#[derive(Debug, Clone)]
580pub struct BandwidthHandle {
581 buckets: Vec<Arc<BandwidthBucket>>,
582 total_bytes: Arc<AtomicU64>,
583 epoch: Instant,
584}
585
586impl BandwidthHandle {
587 pub fn record(&self, bytes: u64) {
589 let now_nanos = self.epoch.elapsed().as_nanos() as u64;
590 self.total_bytes.fetch_add(bytes, Ordering::Relaxed);
591 for bucket in &self.buckets {
592 bucket.spend(now_nanos, bytes);
593 }
594 }
595}
596
597#[derive(Debug)]
598struct ThroughputRateLimiter {
599 config: ThroughputLimits,
600 global: Option<Mutex<TokenBucket>>,
601 total_admitted: Arc<AtomicU64>,
603 usecases: Arc<papaya::HashMap<Usecase, Mutex<TokenBucket>>>,
606 scopes: Arc<papaya::HashMap<Scopes, Mutex<TokenBucket>>>,
607 rules: papaya::HashMap<usize, Mutex<TokenBucket>>,
608}
609
610impl ThroughputRateLimiter {
611 fn new(config: ThroughputLimits) -> Self {
612 let global = config
613 .global_rps
614 .map(|rps| Mutex::new(TokenBucket::new(rps, config.burst)));
615
616 Self {
617 config,
618 global,
619 total_admitted: Arc::new(AtomicU64::new(0)),
620 usecases: Arc::new(papaya::HashMap::new()),
621 scopes: Arc::new(papaya::HashMap::new()),
622 rules: papaya::HashMap::new(),
623 }
624 }
625
626 fn start(&self) {
627 let usecases = Arc::clone(&self.usecases);
628 let scopes = Arc::clone(&self.scopes);
629 let global_limit = self.config.global_rps;
630 tokio::task::spawn(async move {
631 const TICK: Duration = Duration::from_secs(1);
632 let mut interval = tokio::time::interval(TICK);
633 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
634 interval.tick().await;
635 loop {
636 interval.tick().await;
637 if let Some(limit) = global_limit {
638 objectstore_metrics::gauge!(
639 "server.rate_limiter.throughput.limit" = u64::from(limit)
640 );
641 }
642 objectstore_metrics::gauge!(
643 "server.rate_limiter.throughput.scope_map_size" = scopes.len()
644 );
645 objectstore_metrics::gauge!(
646 "server.rate_limiter.throughput.usecase_map_size" = usecases.len()
647 );
648 }
649 });
650 }
651
652 fn check(&self, context: &ObjectContext) -> Option<RateLimitRejection> {
653 if let Some(ref global) = self.global {
657 let acquired = global.lock().unwrap().try_acquire();
658 if !acquired {
659 return Some(RateLimitRejection::ThroughputGlobal);
660 }
661 }
662
663 if let Some(usecase_rps) = self.usecase_rps() {
665 let guard = self.usecases.pin();
666 let bucket = guard
667 .get_or_insert_with(context.usecase.clone(), || self.create_bucket(usecase_rps));
668 if !bucket.lock().unwrap().try_acquire() {
669 return Some(RateLimitRejection::ThroughputUsecase);
670 }
671 }
672
673 if let Some(scope_rps) = self.scope_rps() {
675 let guard = self.scopes.pin();
676 let bucket =
677 guard.get_or_insert_with(context.scopes.clone(), || self.create_bucket(scope_rps));
678 if !bucket.lock().unwrap().try_acquire() {
679 return Some(RateLimitRejection::ThroughputScope);
680 }
681 }
682
683 for (idx, rule) in self.config.rules.iter().enumerate() {
685 if !rule.matches(context) {
686 continue;
687 }
688 let Some(rule_rps) = self.rule_rps(rule) else {
689 continue;
690 };
691 let guard = self.rules.pin();
692 let bucket = guard.get_or_insert_with(idx, || self.create_bucket(rule_rps));
693 if !bucket.lock().unwrap().try_acquire() {
694 return Some(RateLimitRejection::ThroughputRule);
695 }
696 }
697
698 self.total_admitted.fetch_add(1, Ordering::Relaxed);
699
700 None
701 }
702
703 fn create_bucket(&self, rps: u32) -> Mutex<TokenBucket> {
704 Mutex::new(TokenBucket::new(rps, self.config.burst))
705 }
706
707 fn usecase_rps(&self) -> Option<u32> {
709 let global_rps = self.config.global_rps?;
710 let pct = self.config.usecase_pct?;
711 Some(pct_of_u32(global_rps, pct))
712 }
713
714 fn scope_rps(&self) -> Option<u32> {
716 let global_rps = self.config.global_rps?;
717 let pct = self.config.scope_pct?;
718 Some(pct_of_u32(global_rps, pct))
719 }
720
721 fn rule_rps(&self, rule: &ThroughputRule) -> Option<u32> {
723 let pct_limit = rule
724 .pct
725 .and_then(|p| self.config.global_rps.map(|g| pct_of_u32(g, p)));
726
727 match (rule.rps, pct_limit) {
728 (Some(r), Some(p)) => Some(r.min(p)),
729 (Some(r), None) => Some(r),
730 (None, Some(p)) => Some(p),
731 (None, None) => None,
732 }
733 }
734}
735
736#[derive(Debug)]
743struct TokenBucket {
744 refill_rate: f64,
745 capacity: f64,
746 tokens: f64,
747 last_update: Instant,
748}
749
750impl TokenBucket {
751 pub fn new(rps: u32, burst: u32) -> Self {
756 Self {
757 refill_rate: rps as f64,
758 capacity: (rps + burst) as f64,
759 tokens: (rps + burst) as f64,
760 last_update: Instant::now(),
761 }
762 }
763
764 pub fn try_acquire(&mut self) -> bool {
768 let now = Instant::now();
769 let refill = now.duration_since(self.last_update).as_secs_f64() * self.refill_rate;
770 let refilled = (self.tokens + refill).min(self.capacity);
771
772 if refilled.floor() > self.tokens.floor() {
774 self.last_update = now;
775 self.tokens = refilled;
776 }
777
778 if self.tokens >= 1.0 {
780 self.tokens -= 1.0;
781 true
782 } else {
783 false
784 }
785 }
786}
787
788pub(crate) struct MeteredPayloadStream<S> {
794 inner: S,
795 handle: BandwidthHandle,
796}
797
798impl<S> MeteredPayloadStream<S> {
799 pub fn new(inner: S, handle: BandwidthHandle) -> Self {
800 Self { inner, handle }
801 }
802}
803
804impl<S> fmt::Debug for MeteredPayloadStream<S> {
805 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806 f.debug_struct("MeteredPayloadStream")
807 .field("handle", &self.handle)
808 .finish()
809 }
810}
811
812impl<S, E> Stream for MeteredPayloadStream<S>
813where
814 S: Stream<Item = Result<Bytes, E>> + Unpin,
815{
816 type Item = Result<Bytes, E>;
817
818 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
819 let this = self.get_mut();
820 let res = Pin::new(&mut this.inner).poll_next(cx);
821 if let Poll::Ready(Some(Ok(ref bytes))) = res {
822 let len = bytes.len() as u64;
823 this.handle.record(len);
824 objectstore_metrics::count!("server.bandwidth.bytes" += len);
825 }
826 res
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use objectstore_service::id::ObjectContext;
833 use objectstore_types::scope::{Scope, Scopes};
834
835 use super::*;
836
837 fn make_context() -> ObjectContext {
838 ObjectContext {
839 usecase: "testing".parse().unwrap(),
840 scopes: Scopes::from_iter([Scope::create("org", "1").unwrap()]),
841 }
842 }
843
844 #[test]
845 fn throughput_rule_deserialization_validates_usecase() {
846 let rule: ThroughputRule = serde_json::from_value(serde_json::json!({
847 "usecase": "testing",
848 "scopes": [],
849 "rps": 10,
850 }))
851 .unwrap();
852 assert!(rule.matches(&make_context()));
853 assert_eq!(serde_json::to_value(&rule).unwrap()["usecase"], "testing");
854
855 let wildcard: ThroughputRule = serde_json::from_value(serde_json::json!({
856 "scopes": [],
857 "rps": 10,
858 }))
859 .unwrap();
860 assert!(wildcard.matches(&make_context()));
861 assert!(
862 serde_json::from_value::<ThroughputRule>(serde_json::json!({
863 "usecase": "test/name",
864 "scopes": [],
865 "rps": 10,
866 }))
867 .is_err()
868 );
869 }
870
871 #[test]
874 fn bucket_spend_advances_tat() {
875 let bucket = BandwidthBucket::new(1000, 0); let now = 1_000_000_000u64; bucket.spend(now, 500);
879
880 let expected_tat = now + 500_000_000;
882 assert_eq!(bucket.tat.load(Ordering::Relaxed), expected_tat);
883 }
884
885 #[test]
886 fn bucket_spend_clamps_credit() {
887 let bucket = BandwidthBucket::new(1000, 0);
888 bucket.tat.store(100, Ordering::Relaxed);
890 let now = 2_000_000_000u64;
891
892 bucket.spend(now, 100);
893
894 let weight = (100.0 * 1_000_000_000.0 / 1000.0) as u64;
896 let expected = now + weight;
897 assert_eq!(bucket.tat.load(Ordering::Relaxed), expected);
898 }
899
900 #[test]
901 fn bucket_check_admits_within_burst() {
902 let bucket = BandwidthBucket::new(1000, 1000); let now = 1_000_000_000u64;
904
905 bucket.spend(now, 1500);
907
908 assert!(!bucket.check(now));
910
911 assert!(bucket.check(now + 600_000_000));
913 }
914
915 #[test]
916 fn bucket_check_zero_burst_rejects_any_debt() {
917 let bucket = BandwidthBucket::new(1000, 0);
918 let now = 1_000_000_000u64;
919
920 bucket.spend(now, 1);
921
922 assert!(!bucket.check(now));
924
925 let weight = (1.0 * 1_000_000_000.0 / 1000.0) as u64;
927 assert!(bucket.check(now + weight));
928 }
929
930 #[test]
931 fn bucket_recovery_after_debt() {
932 let bucket = BandwidthBucket::new(1000, 0);
933 let now = 1_000_000_000u64;
934
935 bucket.spend(now, 2000);
936
937 assert!(!bucket.check(now));
939 assert!(!bucket.check(now + 1_500_000_000));
940
941 assert!(bucket.check(now + 2_000_000_000));
943 }
944
945 #[test]
946 fn bandwidth_check_rejects_correct_variant() {
947 let limiter = BandwidthRateLimiter::new(BandwidthLimits {
954 global_bps: Some(1000),
955 usecase_pct: Some(50),
956 scope_pct: Some(25),
957 burst_ms: 2000,
958 ..Default::default()
959 });
960
961 let context = make_context();
962 let handle = limiter.handle(&context);
963 handle.record(600);
964
965 let rejection = limiter.check(&context);
966 assert_eq!(rejection, Some(RateLimitRejection::BandwidthScope));
967 }
968
969 #[test]
972 fn throughput_check_counts_admitted() {
973 let limiter = ThroughputRateLimiter::new(ThroughputLimits {
974 global_rps: Some(1000),
975 ..Default::default()
976 });
977
978 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 0);
979
980 let context = make_context();
981 assert!(limiter.check(&context).is_none());
982 assert!(limiter.check(&context).is_none());
983
984 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 2);
985 }
986
987 #[test]
988 fn throughput_rejected_does_not_count() {
989 let limiter = ThroughputRateLimiter::new(ThroughputLimits {
990 global_rps: Some(1),
991 burst: 0,
992 ..Default::default()
993 });
994
995 let context = make_context();
996 assert!(limiter.check(&context).is_none());
998 assert!(limiter.check(&context).is_some());
999
1000 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 1);
1001 }
1002
1003 #[test]
1004 fn bandwidth_rejection_does_not_count_throughput() {
1005 let limiter = RateLimiter::new(RateLimits {
1006 throughput: ThroughputLimits {
1007 global_rps: Some(1000),
1008 ..Default::default()
1009 },
1010 bandwidth: BandwidthLimits {
1011 global_bps: Some(1),
1012 burst_ms: 0,
1013 ..Default::default()
1014 },
1015 });
1016
1017 let context = make_context();
1019 let handle = limiter.bandwidth_handle(&context);
1020 handle.record(1_000_000);
1021
1022 assert_eq!(limiter.check(&context, None), Err(LimitExceeded::Capacity));
1023
1024 assert_eq!(limiter.throughput.total_admitted.load(Ordering::Relaxed), 0);
1025 }
1026
1027 #[test]
1028 fn rate_limiter_accessors_with_config() {
1029 let rate_limiter = RateLimiter::new(RateLimits {
1030 throughput: ThroughputLimits {
1031 global_rps: Some(500),
1032 ..Default::default()
1033 },
1034 bandwidth: BandwidthLimits {
1035 global_bps: Some(1_000_000),
1036 ..Default::default()
1037 },
1038 });
1039
1040 assert_eq!(rate_limiter.bandwidth_limit(), Some(1_000_000));
1041 assert_eq!(rate_limiter.throughput_limit(), Some(500));
1042 }
1043
1044 #[test]
1045 fn rate_limiter_accessors_no_limits() {
1046 let rate_limiter = RateLimiter::new(RateLimits::default());
1047
1048 assert_eq!(rate_limiter.bandwidth_limit(), None);
1049 assert_eq!(rate_limiter.throughput_limit(), None);
1050 }
1051}