1use std::fmt;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::sync::Mutex;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::task::{Context, Poll};
17use std::time::{Duration, Instant};
18
19use bytes::Bytes;
20use futures_util::Stream;
21use objectstore_service::id::ObjectContext;
22use objectstore_types::scope::Scopes;
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum RateLimitRejection {
28 BandwidthGlobal,
30 BandwidthUsecase,
32 BandwidthScope,
34 ThroughputGlobal,
36 ThroughputUsecase,
38 ThroughputScope,
40 ThroughputRule,
42}
43
44impl RateLimitRejection {
45 pub fn as_str(self) -> &'static str {
47 match self {
48 RateLimitRejection::BandwidthGlobal => "bandwidth_global",
49 RateLimitRejection::BandwidthUsecase => "bandwidth_usecase",
50 RateLimitRejection::BandwidthScope => "bandwidth_scope",
51 RateLimitRejection::ThroughputGlobal => "throughput_global",
52 RateLimitRejection::ThroughputUsecase => "throughput_usecase",
53 RateLimitRejection::ThroughputScope => "throughput_scope",
54 RateLimitRejection::ThroughputRule => "throughput_rule",
55 }
56 }
57}
58
59impl fmt::Display for RateLimitRejection {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(self.as_str())
62 }
63}
64
65#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
67pub struct RateLimits {
68 pub throughput: ThroughputLimits,
70 pub bandwidth: BandwidthLimits,
72}
73
74#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
79pub struct ThroughputLimits {
80 pub global_rps: Option<u32>,
84
85 pub burst: u32,
90
91 pub usecase_pct: Option<u8>,
95
96 pub scope_pct: Option<u8>,
104
105 pub rules: Vec<ThroughputRule>,
107}
108
109#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
115pub struct ThroughputRule {
116 pub usecase: Option<String>,
120
121 pub scopes: Vec<(String, String)>,
126
127 pub rps: Option<u32>,
132
133 pub pct: Option<u8>,
138}
139
140impl ThroughputRule {
141 pub fn matches(&self, context: &ObjectContext) -> bool {
143 if let Some(ref rule_usecase) = self.usecase
144 && rule_usecase != &context.usecase
145 {
146 return false;
147 }
148
149 for (scope_name, scope_value) in &self.scopes {
150 match context.scopes.get_value(scope_name) {
151 Some(value) if value == scope_value => (),
152 _ => return false,
153 }
154 }
155
156 true
157 }
158}
159
160#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
165pub struct BandwidthLimits {
166 pub global_bps: Option<u64>,
170
171 #[serde(default = "default_burst_ms")]
176 pub burst_ms: u64,
177
178 pub usecase_pct: Option<u8>,
182
183 pub scope_pct: Option<u8>,
187
188 #[serde(default)]
193 pub report_only: bool,
194}
195
196fn default_burst_ms() -> u64 {
197 1000
198}
199
200impl Default for BandwidthLimits {
201 fn default() -> Self {
202 Self {
203 global_bps: None,
204 burst_ms: default_burst_ms(),
205 usecase_pct: None,
206 scope_pct: None,
207 report_only: false,
208 }
209 }
210}
211
212#[derive(Debug)]
221pub struct RateLimiter {
222 bandwidth: BandwidthRateLimiter,
223 throughput: ThroughputRateLimiter,
224}
225
226impl RateLimiter {
227 pub fn new(config: RateLimits) -> Self {
231 Self {
232 bandwidth: BandwidthRateLimiter::new(config.bandwidth),
233 throughput: ThroughputRateLimiter::new(config.throughput),
234 }
235 }
236
237 pub fn start(&self) {
241 self.throughput.start();
242 }
243
244 pub fn check(&self, context: &ObjectContext, key: Option<&str>) -> bool {
250 let rejection = self
254 .bandwidth
255 .check(context)
256 .or_else(|| self.throughput.check(context));
257
258 let Some(rejection) = rejection else {
259 return true;
260 };
261
262 objectstore_metrics::count!(
263 "server.request.rate_limited",
264 reason = rejection.as_str(),
265 usecase = context.usecase.clone()
266 );
267 objectstore_log::warn!(
268 reason = rejection.as_str(),
269 usecase = &context.usecase,
270 scopes = %context.scopes.as_api_path(),
271 key,
272 "Request rejected: rate limit exceeded"
273 );
274 false
275 }
276
277 pub fn bandwidth_handle(&self, context: &ObjectContext) -> BandwidthHandle {
281 self.bandwidth.handle(context)
282 }
283
284 pub fn record_bandwidth(&self, context: &ObjectContext, bytes: u64) {
289 self.bandwidth.handle(context).record(bytes);
290 objectstore_metrics::count!("server.bandwidth.bytes" += bytes);
291 }
292
293 pub fn bandwidth_limit(&self) -> Option<u64> {
295 self.bandwidth.config.global_bps
296 }
297
298 pub fn throughput_limit(&self) -> Option<u32> {
300 self.throughput.config.global_rps
301 }
302
303 pub fn bandwidth_total_bytes(&self) -> u64 {
305 self.bandwidth.total_bytes.load(Ordering::Relaxed)
306 }
307
308 pub fn throughput_total_admitted(&self) -> u64 {
310 self.throughput.total_admitted.load(Ordering::Relaxed)
311 }
312}
313
314#[derive(Debug)]
321struct BandwidthBucket {
322 tat: AtomicU64,
326 nanos_per_byte: f64,
328 burst_ns: u64,
330}
331
332impl BandwidthBucket {
333 fn new(bps: u64, burst_ms: u64) -> Self {
335 let nanos_per_byte = 1_000_000_000.0 / bps as f64;
336 let burst_ns = burst_ms * 1_000_000;
337 Self {
338 tat: AtomicU64::new(0),
339 nanos_per_byte,
340 burst_ns,
341 }
342 }
343
344 fn spend(&self, now_nanos: u64, bytes: u64) {
346 let weight = (bytes as f64 * self.nanos_per_byte) as u64;
347 self.tat
350 .update(Ordering::Relaxed, Ordering::Relaxed, |old| {
351 old.max(now_nanos).saturating_add(weight)
352 });
353 }
354
355 fn check(&self, now_nanos: u64) -> bool {
357 self.tat.load(Ordering::Relaxed) <= now_nanos.saturating_add(self.burst_ns)
358 }
359}
360
361fn pct_of_u32(value: u32, pct: u8) -> u32 {
363 let scaled = u64::from(value) * u64::from(pct) / 100;
364 u32::try_from(scaled).unwrap_or(u32::MAX)
365}
366
367fn pct_of_u64(value: u64, pct: u8) -> u64 {
369 let scaled = u128::from(value) * u128::from(pct) / 100;
370 u64::try_from(scaled).unwrap_or(u64::MAX)
371}
372
373#[derive(Debug)]
374struct BandwidthRateLimiter {
375 config: BandwidthLimits,
376 global: Option<Arc<BandwidthBucket>>,
378 epoch: Instant,
380 total_bytes: Arc<AtomicU64>,
382 usecases: Arc<papaya::HashMap<String, Arc<BandwidthBucket>>>,
385 scopes: Arc<papaya::HashMap<Scopes, Arc<BandwidthBucket>>>,
386}
387
388impl BandwidthRateLimiter {
389 fn new(config: BandwidthLimits) -> Self {
390 let global = config
391 .global_bps
392 .map(|bps| Arc::new(BandwidthBucket::new(bps, config.burst_ms)));
393
394 if let Some(limit) = config.global_bps {
395 objectstore_metrics::gauge!("server.bandwidth.limit" = limit);
396 }
397
398 Self {
399 global,
400 epoch: Instant::now(),
401 total_bytes: Arc::new(AtomicU64::new(0)),
402 usecases: Arc::new(papaya::HashMap::new()),
403 scopes: Arc::new(papaya::HashMap::new()),
404 config,
405 }
406 }
407
408 fn now_nanos(&self) -> u64 {
410 self.epoch.elapsed().as_nanos() as u64
412 }
413
414 fn check(&self, context: &ObjectContext) -> Option<RateLimitRejection> {
418 if self.config.report_only {
419 return None;
420 }
421
422 let now_nanos = self.now_nanos();
423
424 if let Some(ref global) = self.global
426 && !global.check(now_nanos)
427 {
428 return Some(RateLimitRejection::BandwidthGlobal);
429 }
430
431 if self.usecase_bps().is_some() {
433 let guard = self.usecases.pin();
434 if let Some(bucket) = guard.get(&context.usecase)
435 && !bucket.check(now_nanos)
436 {
437 return Some(RateLimitRejection::BandwidthUsecase);
438 }
439 }
440
441 if self.scope_bps().is_some() {
443 let guard = self.scopes.pin();
444 if let Some(bucket) = guard.get(&context.scopes)
445 && !bucket.check(now_nanos)
446 {
447 return Some(RateLimitRejection::BandwidthScope);
448 }
449 }
450
451 None
452 }
453
454 fn handle(&self, context: &ObjectContext) -> BandwidthHandle {
456 let mut buckets = Vec::new();
457
458 if let Some(ref global) = self.global {
459 buckets.push(Arc::clone(global));
460 }
461
462 if let Some(usecase_bps) = self.usecase_bps() {
463 let guard = self.usecases.pin();
464 let bucket = guard.get_or_insert_with(context.usecase.clone(), || {
465 Arc::new(BandwidthBucket::new(usecase_bps, self.config.burst_ms))
466 });
467 buckets.push(Arc::clone(bucket));
468 }
469
470 if let Some(scope_bps) = self.scope_bps() {
471 let guard = self.scopes.pin();
472 let bucket = guard.get_or_insert_with(context.scopes.clone(), || {
473 Arc::new(BandwidthBucket::new(scope_bps, self.config.burst_ms))
474 });
475 buckets.push(Arc::clone(bucket));
476 }
477
478 objectstore_metrics::gauge!(
479 "server.rate_limiter.bandwidth.usecase_map_size" = self.usecases.len()
480 );
481 objectstore_metrics::gauge!(
482 "server.rate_limiter.bandwidth.scope_map_size" = self.scopes.len()
483 );
484
485 BandwidthHandle {
486 buckets,
487 total_bytes: Arc::clone(&self.total_bytes),
488 epoch: self.epoch,
489 }
490 }
491
492 fn usecase_bps(&self) -> Option<u64> {
494 let global_bps = self.config.global_bps?;
495 let pct = self.config.usecase_pct?;
496 Some(pct_of_u64(global_bps, pct))
497 }
498
499 fn scope_bps(&self) -> Option<u64> {
501 let global_bps = self.config.global_bps?;
502 let pct = self.config.scope_pct?;
503 Some(pct_of_u64(global_bps, pct))
504 }
505}
506
507#[derive(Debug, Clone)]
509pub struct BandwidthHandle {
510 buckets: Vec<Arc<BandwidthBucket>>,
511 total_bytes: Arc<AtomicU64>,
512 epoch: Instant,
513}
514
515impl BandwidthHandle {
516 pub fn record(&self, bytes: u64) {
518 let now_nanos = self.epoch.elapsed().as_nanos() as u64;
519 self.total_bytes.fetch_add(bytes, Ordering::Relaxed);
520 for bucket in &self.buckets {
521 bucket.spend(now_nanos, bytes);
522 }
523 }
524}
525
526#[derive(Debug)]
527struct ThroughputRateLimiter {
528 config: ThroughputLimits,
529 global: Option<Mutex<TokenBucket>>,
530 total_admitted: Arc<AtomicU64>,
532 usecases: Arc<papaya::HashMap<String, Mutex<TokenBucket>>>,
535 scopes: Arc<papaya::HashMap<Scopes, Mutex<TokenBucket>>>,
536 rules: papaya::HashMap<usize, Mutex<TokenBucket>>,
537}
538
539impl ThroughputRateLimiter {
540 fn new(config: ThroughputLimits) -> Self {
541 let global = config
542 .global_rps
543 .map(|rps| Mutex::new(TokenBucket::new(rps, config.burst)));
544
545 Self {
546 config,
547 global,
548 total_admitted: Arc::new(AtomicU64::new(0)),
549 usecases: Arc::new(papaya::HashMap::new()),
550 scopes: Arc::new(papaya::HashMap::new()),
551 rules: papaya::HashMap::new(),
552 }
553 }
554
555 fn start(&self) {
556 let usecases = Arc::clone(&self.usecases);
557 let scopes = Arc::clone(&self.scopes);
558 let global_limit = self.config.global_rps;
559 tokio::task::spawn(async move {
560 const TICK: Duration = Duration::from_secs(1);
561 let mut interval = tokio::time::interval(TICK);
562 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
563 interval.tick().await;
564 loop {
565 interval.tick().await;
566 if let Some(limit) = global_limit {
567 objectstore_metrics::gauge!(
568 "server.rate_limiter.throughput.limit" = u64::from(limit)
569 );
570 }
571 objectstore_metrics::gauge!(
572 "server.rate_limiter.throughput.scope_map_size" = scopes.len()
573 );
574 objectstore_metrics::gauge!(
575 "server.rate_limiter.throughput.usecase_map_size" = usecases.len()
576 );
577 }
578 });
579 }
580
581 fn check(&self, context: &ObjectContext) -> Option<RateLimitRejection> {
582 if let Some(ref global) = self.global {
586 let acquired = global.lock().unwrap().try_acquire();
587 if !acquired {
588 return Some(RateLimitRejection::ThroughputGlobal);
589 }
590 }
591
592 if let Some(usecase_rps) = self.usecase_rps() {
594 let guard = self.usecases.pin();
595 let bucket = guard
596 .get_or_insert_with(context.usecase.clone(), || self.create_bucket(usecase_rps));
597 if !bucket.lock().unwrap().try_acquire() {
598 return Some(RateLimitRejection::ThroughputUsecase);
599 }
600 }
601
602 if let Some(scope_rps) = self.scope_rps() {
604 let guard = self.scopes.pin();
605 let bucket =
606 guard.get_or_insert_with(context.scopes.clone(), || self.create_bucket(scope_rps));
607 if !bucket.lock().unwrap().try_acquire() {
608 return Some(RateLimitRejection::ThroughputScope);
609 }
610 }
611
612 for (idx, rule) in self.config.rules.iter().enumerate() {
614 if !rule.matches(context) {
615 continue;
616 }
617 let Some(rule_rps) = self.rule_rps(rule) else {
618 continue;
619 };
620 let guard = self.rules.pin();
621 let bucket = guard.get_or_insert_with(idx, || self.create_bucket(rule_rps));
622 if !bucket.lock().unwrap().try_acquire() {
623 return Some(RateLimitRejection::ThroughputRule);
624 }
625 }
626
627 self.total_admitted.fetch_add(1, Ordering::Relaxed);
628
629 None
630 }
631
632 fn create_bucket(&self, rps: u32) -> Mutex<TokenBucket> {
633 Mutex::new(TokenBucket::new(rps, self.config.burst))
634 }
635
636 fn usecase_rps(&self) -> Option<u32> {
638 let global_rps = self.config.global_rps?;
639 let pct = self.config.usecase_pct?;
640 Some(pct_of_u32(global_rps, pct))
641 }
642
643 fn scope_rps(&self) -> Option<u32> {
645 let global_rps = self.config.global_rps?;
646 let pct = self.config.scope_pct?;
647 Some(pct_of_u32(global_rps, pct))
648 }
649
650 fn rule_rps(&self, rule: &ThroughputRule) -> Option<u32> {
652 let pct_limit = rule
653 .pct
654 .and_then(|p| self.config.global_rps.map(|g| pct_of_u32(g, p)));
655
656 match (rule.rps, pct_limit) {
657 (Some(r), Some(p)) => Some(r.min(p)),
658 (Some(r), None) => Some(r),
659 (None, Some(p)) => Some(p),
660 (None, None) => None,
661 }
662 }
663}
664
665#[derive(Debug)]
672struct TokenBucket {
673 refill_rate: f64,
674 capacity: f64,
675 tokens: f64,
676 last_update: Instant,
677}
678
679impl TokenBucket {
680 pub fn new(rps: u32, burst: u32) -> Self {
685 Self {
686 refill_rate: rps as f64,
687 capacity: (rps + burst) as f64,
688 tokens: (rps + burst) as f64,
689 last_update: Instant::now(),
690 }
691 }
692
693 pub fn try_acquire(&mut self) -> bool {
697 let now = Instant::now();
698 let refill = now.duration_since(self.last_update).as_secs_f64() * self.refill_rate;
699 let refilled = (self.tokens + refill).min(self.capacity);
700
701 if refilled.floor() > self.tokens.floor() {
703 self.last_update = now;
704 self.tokens = refilled;
705 }
706
707 if self.tokens >= 1.0 {
709 self.tokens -= 1.0;
710 true
711 } else {
712 false
713 }
714 }
715}
716
717pub(crate) struct MeteredPayloadStream<S> {
723 inner: S,
724 handle: BandwidthHandle,
725}
726
727impl<S> MeteredPayloadStream<S> {
728 pub fn new(inner: S, handle: BandwidthHandle) -> Self {
729 Self { inner, handle }
730 }
731}
732
733impl<S> fmt::Debug for MeteredPayloadStream<S> {
734 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735 f.debug_struct("MeteredPayloadStream")
736 .field("handle", &self.handle)
737 .finish()
738 }
739}
740
741impl<S, E> Stream for MeteredPayloadStream<S>
742where
743 S: Stream<Item = Result<Bytes, E>> + Unpin,
744{
745 type Item = Result<Bytes, E>;
746
747 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
748 let this = self.get_mut();
749 let res = Pin::new(&mut this.inner).poll_next(cx);
750 if let Poll::Ready(Some(Ok(ref bytes))) = res {
751 let len = bytes.len() as u64;
752 this.handle.record(len);
753 objectstore_metrics::count!("server.bandwidth.bytes" += len);
754 }
755 res
756 }
757}
758
759#[cfg(test)]
760mod tests {
761 use objectstore_service::id::ObjectContext;
762 use objectstore_types::scope::{Scope, Scopes};
763
764 use super::*;
765
766 fn make_context() -> ObjectContext {
767 ObjectContext {
768 usecase: "testing".into(),
769 scopes: Scopes::from_iter([Scope::create("org", "1").unwrap()]),
770 }
771 }
772
773 #[test]
776 fn bucket_spend_advances_tat() {
777 let bucket = BandwidthBucket::new(1000, 0); let now = 1_000_000_000u64; bucket.spend(now, 500);
781
782 let expected_tat = now + 500_000_000;
784 assert_eq!(bucket.tat.load(Ordering::Relaxed), expected_tat);
785 }
786
787 #[test]
788 fn bucket_spend_clamps_credit() {
789 let bucket = BandwidthBucket::new(1000, 0);
790 bucket.tat.store(100, Ordering::Relaxed);
792 let now = 2_000_000_000u64;
793
794 bucket.spend(now, 100);
795
796 let weight = (100.0 * 1_000_000_000.0 / 1000.0) as u64;
798 let expected = now + weight;
799 assert_eq!(bucket.tat.load(Ordering::Relaxed), expected);
800 }
801
802 #[test]
803 fn bucket_check_admits_within_burst() {
804 let bucket = BandwidthBucket::new(1000, 1000); let now = 1_000_000_000u64;
806
807 bucket.spend(now, 1500);
809
810 assert!(!bucket.check(now));
812
813 assert!(bucket.check(now + 600_000_000));
815 }
816
817 #[test]
818 fn bucket_check_zero_burst_rejects_any_debt() {
819 let bucket = BandwidthBucket::new(1000, 0);
820 let now = 1_000_000_000u64;
821
822 bucket.spend(now, 1);
823
824 assert!(!bucket.check(now));
826
827 let weight = (1.0 * 1_000_000_000.0 / 1000.0) as u64;
829 assert!(bucket.check(now + weight));
830 }
831
832 #[test]
833 fn bucket_recovery_after_debt() {
834 let bucket = BandwidthBucket::new(1000, 0);
835 let now = 1_000_000_000u64;
836
837 bucket.spend(now, 2000);
838
839 assert!(!bucket.check(now));
841 assert!(!bucket.check(now + 1_500_000_000));
842
843 assert!(bucket.check(now + 2_000_000_000));
845 }
846
847 #[test]
848 fn bandwidth_check_rejects_correct_variant() {
849 let limiter = BandwidthRateLimiter::new(BandwidthLimits {
856 global_bps: Some(1000),
857 usecase_pct: Some(50),
858 scope_pct: Some(25),
859 burst_ms: 2000,
860 ..Default::default()
861 });
862
863 let context = make_context();
864 let handle = limiter.handle(&context);
865 handle.record(600);
866
867 let rejection = limiter.check(&context);
868 assert_eq!(rejection, Some(RateLimitRejection::BandwidthScope));
869 }
870
871 #[test]
874 fn throughput_check_counts_admitted() {
875 let limiter = ThroughputRateLimiter::new(ThroughputLimits {
876 global_rps: Some(1000),
877 ..Default::default()
878 });
879
880 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 0);
881
882 let context = make_context();
883 assert!(limiter.check(&context).is_none());
884 assert!(limiter.check(&context).is_none());
885
886 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 2);
887 }
888
889 #[test]
890 fn throughput_rejected_does_not_count() {
891 let limiter = ThroughputRateLimiter::new(ThroughputLimits {
892 global_rps: Some(1),
893 burst: 0,
894 ..Default::default()
895 });
896
897 let context = make_context();
898 assert!(limiter.check(&context).is_none());
900 assert!(limiter.check(&context).is_some());
901
902 assert_eq!(limiter.total_admitted.load(Ordering::Relaxed), 1);
903 }
904
905 #[test]
906 fn bandwidth_rejection_does_not_count_throughput() {
907 let limiter = RateLimiter::new(RateLimits {
908 throughput: ThroughputLimits {
909 global_rps: Some(1000),
910 ..Default::default()
911 },
912 bandwidth: BandwidthLimits {
913 global_bps: Some(1),
914 burst_ms: 0,
915 ..Default::default()
916 },
917 });
918
919 let context = make_context();
921 let handle = limiter.bandwidth_handle(&context);
922 handle.record(1_000_000);
923
924 assert!(!limiter.check(&context, None));
925
926 assert_eq!(limiter.throughput.total_admitted.load(Ordering::Relaxed), 0);
927 }
928
929 #[test]
930 fn rate_limiter_accessors_with_config() {
931 let rate_limiter = RateLimiter::new(RateLimits {
932 throughput: ThroughputLimits {
933 global_rps: Some(500),
934 ..Default::default()
935 },
936 bandwidth: BandwidthLimits {
937 global_bps: Some(1_000_000),
938 ..Default::default()
939 },
940 });
941
942 assert_eq!(rate_limiter.bandwidth_limit(), Some(1_000_000));
943 assert_eq!(rate_limiter.throughput_limit(), Some(500));
944 }
945
946 #[test]
947 fn rate_limiter_accessors_no_limits() {
948 let rate_limiter = RateLimiter::new(RateLimits::default());
949
950 assert_eq!(rate_limiter.bandwidth_limit(), None);
951 assert_eq!(rate_limiter.throughput_limit(), None);
952 }
953}