Skip to main content

objectstore_server/
rate_limits.rs

1//! Admission-based rate limiting for throughput and bandwidth.
2//!
3//! This module provides [`RateLimiter`], which enforces configurable limits at three
4//! levels of granularity — global, per-usecase, and per-scope — for both request
5//! throughput (requests/s) and upload/download bandwidth (bytes/s).
6//!
7//! Throughput is enforced using token buckets. Bandwidth uses debt-based GCRA
8//! (Generic Cell Rate Algorithm) buckets that track a theoretical arrival time
9//! (TAT). All rate-limit checks are synchronous, non-blocking, and lock-free.
10
11use 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/// Identifies which rate limit triggered a rejection.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum RateLimitRejection {
28    /// Global bandwidth limit exceeded.
29    BandwidthGlobal,
30    /// Per-usecase bandwidth limit exceeded.
31    BandwidthUsecase,
32    /// Per-scope bandwidth limit exceeded.
33    BandwidthScope,
34    /// Global throughput limit exceeded.
35    ThroughputGlobal,
36    /// Per-usecase throughput limit exceeded.
37    ThroughputUsecase,
38    /// Per-scope throughput limit exceeded.
39    ThroughputScope,
40    /// Per-rule throughput limit exceeded.
41    ThroughputRule,
42}
43
44impl RateLimitRejection {
45    /// Returns a static string identifier suitable for use as a metric tag.
46    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/// Rate limits for objectstore.
66#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
67pub struct RateLimits {
68    /// Limits the number of requests per second per service instance.
69    pub throughput: ThroughputLimits,
70    /// Limits the concurrent bandwidth per service instance.
71    pub bandwidth: BandwidthLimits,
72}
73
74/// Request throughput limits applied at global, per-usecase, and per-scope granularity.
75///
76/// All limits are optional. When a limit is `None`, that level of limiting is not enforced.
77/// Per-usecase and per-scope limits are expressed as a percentage of the global limit.
78#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
79pub struct ThroughputLimits {
80    /// The overall maximum number of requests per second per service instance.
81    ///
82    /// Defaults to `None`, meaning no global rate limit is enforced.
83    pub global_rps: Option<u32>,
84
85    /// The maximum burst for each rate limit.
86    ///
87    /// Defaults to `0`, meaning no bursting is allowed. If set to a value greater than `0`,
88    /// short spikes above the rate limit are allowed up to the burst size.
89    pub burst: u32,
90
91    /// The maximum percentage of the global rate limit that can be used by any usecase.
92    ///
93    /// Value from `0` to `100`. Defaults to `None`, meaning no per-usecase limit is enforced.
94    pub usecase_pct: Option<u8>,
95
96    /// The maximum percentage of the global rate limit that can be used by any scope.
97    ///
98    /// This treats each full scope separately and applies across all use cases:
99    ///  - Two requests with exact same scopes count against the same limit regardless of use case.
100    ///  - Two requests that share the same top scope but differ in inner scopes count separately.
101    ///
102    /// Value from `0` to `100`. Defaults to `None`, meaning no per-scope limit is enforced.
103    pub scope_pct: Option<u8>,
104
105    /// Overrides for specific usecases and scopes.
106    pub rules: Vec<ThroughputRule>,
107}
108
109/// An override rule that applies a specific throughput limit to matching request contexts.
110///
111/// A rule matches when all specified fields match the request context. Fields not set match
112/// any value. When multiple rules match, each is enforced independently via its own token
113/// bucket — all matching rules must admit the request.
114#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
115pub struct ThroughputRule {
116    /// Optional usecase to match.
117    ///
118    /// If `None`, matches any usecase.
119    pub usecase: Option<String>,
120
121    /// Scopes to match.
122    ///
123    /// If empty, matches any scopes. Additional scopes in the context are ignored, so a rule
124    /// matches if all of the specified scopes are present in the request with matching values.
125    pub scopes: Vec<(String, String)>,
126
127    /// The rate limit to apply when this rule matches.
128    ///
129    /// If both a rate and pct are specified, the more restrictive limit applies.
130    /// Should be greater than `0`. To block traffic entirely, use killswitches instead.
131    pub rps: Option<u32>,
132
133    /// The percentage of the global rate limit to apply when this rule matches.
134    ///
135    /// If both a rate and pct are specified, the more restrictive limit applies.
136    /// Should be greater than `0`. To block traffic entirely, use killswitches instead.
137    pub pct: Option<u8>,
138}
139
140impl ThroughputRule {
141    /// Returns `true` if this rule matches the given context.
142    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/// Bandwidth limits applied at global, per-usecase, and per-scope granularity.
161///
162/// Bandwidth is measured as bytes transferred per second (upload + download combined)
163/// and tracked using debt-based GCRA buckets. All limits are optional.
164#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
165pub struct BandwidthLimits {
166    /// The overall maximum bandwidth (in bytes per second) per service instance.
167    ///
168    /// Defaults to `None`, meaning no global bandwidth limit is enforced.
169    pub global_bps: Option<u64>,
170
171    /// Burst tolerance in milliseconds.
172    ///
173    /// Allows short traffic spikes up to `burst_ms * bps / 1000` bytes before
174    /// rejection. Defaults to `1000` (1 second).
175    #[serde(default = "default_burst_ms")]
176    pub burst_ms: u64,
177
178    /// The maximum percentage of the global bandwidth limit that can be used by any usecase.
179    ///
180    /// Value from `0` to `100`. Defaults to `None`, meaning no per-usecase bandwidth limit is enforced.
181    pub usecase_pct: Option<u8>,
182
183    /// The maximum percentage of the global bandwidth limit that can be used by any scope.
184    ///
185    /// Value from `0` to `100`. Defaults to `None`, meaning no per-scope bandwidth limit is enforced.
186    pub scope_pct: Option<u8>,
187
188    /// When `true`, bandwidth limits are evaluated and reported but never enforced.
189    ///
190    /// All accounting and metrics remain active, but requests exceeding the limit
191    /// are not rejected. Defaults to `false`.
192    #[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/// Combined rate limiter that enforces both bandwidth and throughput limits.
213///
214/// Checks are synchronous and non-blocking. Bandwidth is checked before
215/// throughput so that rejected requests are never counted toward the admitted
216/// throughput counter. See [`check`](RateLimiter::check) for details.
217///
218/// Call [`start`](RateLimiter::start) after construction to launch the background
219/// observability tasks.
220#[derive(Debug)]
221pub struct RateLimiter {
222    bandwidth: BandwidthRateLimiter,
223    throughput: ThroughputRateLimiter,
224}
225
226impl RateLimiter {
227    /// Creates a new rate limiter from the given configuration.
228    ///
229    /// Background observability tasks are not started until [`start`](RateLimiter::start) is called.
230    pub fn new(config: RateLimits) -> Self {
231        Self {
232            bandwidth: BandwidthRateLimiter::new(config.bandwidth),
233            throughput: ThroughputRateLimiter::new(config.throughput),
234        }
235    }
236
237    /// Starts background tasks for rate limit monitoring.
238    ///
239    /// Must be called from within a Tokio runtime.
240    pub fn start(&self) {
241        self.throughput.start();
242    }
243
244    /// Checks if the given context is within the rate limits.
245    ///
246    /// Returns `true` if the request is admitted, `false` if it was rejected. On rejection, emits a
247    /// `server.request.rate_limited` metric counter and a `warn!` log. Bandwidth is checked before
248    /// throughput so that rejected requests are never counted toward admitted traffic.
249    pub fn check(&self, context: &ObjectContext, key: Option<&str>) -> bool {
250        // Bandwidth is checked first because it is a pure read (no token consumption).
251        // Throughput counts admitted requests, so checking it second ensures rejected
252        // requests are never counted toward admitted traffic.
253        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    /// Returns bandwidth buckets and the total-bytes counter for the given context.
278    ///
279    /// Creates entries in the per-usecase/per-scope maps if they don't exist yet.
280    pub fn bandwidth_handle(&self, context: &ObjectContext) -> BandwidthHandle {
281        self.bandwidth.handle(context)
282    }
283
284    /// Records bandwidth usage across all buckets for the given context.
285    ///
286    /// This is used for cases where bytes are known upfront (e.g. batch INSERT) rather than
287    /// streamed through a `MeteredPayloadStream`.
288    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    /// Returns the configured global bandwidth limit in bytes/s, if set.
294    pub fn bandwidth_limit(&self) -> Option<u64> {
295        self.bandwidth.config.global_bps
296    }
297
298    /// Returns the configured global throughput limit in requests/s, if set.
299    pub fn throughput_limit(&self) -> Option<u32> {
300        self.throughput.config.global_rps
301    }
302
303    /// Returns total bytes transferred since startup.
304    pub fn bandwidth_total_bytes(&self) -> u64 {
305        self.bandwidth.total_bytes.load(Ordering::Relaxed)
306    }
307
308    /// Returns total admitted requests since startup.
309    pub fn throughput_total_admitted(&self) -> u64 {
310        self.throughput.total_admitted.load(Ordering::Relaxed)
311    }
312}
313
314/// Debt-based GCRA bandwidth bucket.
315///
316/// Tracks a theoretical arrival time (TAT) that advances by `nanos_per_byte`
317/// for every byte consumed. Admission is granted while TAT stays within
318/// `burst_ns` nanoseconds ahead of the wall clock (burst tolerance). Recovery is
319/// continuous — no background tick required.
320#[derive(Debug)]
321struct BandwidthBucket {
322    /// Nanoseconds since `epoch` at which all recorded traffic is paid off.
323    ///
324    /// Saturates at `u64::MAX` after ~584 years of uptime.
325    tat: AtomicU64,
326    /// Nanoseconds of TAT advance per byte: `1e9 / bps`.
327    nanos_per_byte: f64,
328    /// Burst tolerance in nanoseconds: `burst_ms * 1_000_000`.
329    burst_ns: u64,
330}
331
332impl BandwidthBucket {
333    /// Creates a new bucket for the given rate and burst tolerance.
334    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    /// Records `bytes` of consumption unconditionally (debt is allowed).
345    fn spend(&self, now_nanos: u64, bytes: u64) {
346        let weight = (bytes as f64 * self.nanos_per_byte) as u64;
347        // Clamp + advance in one atomic step: never let TAT fall behind `now`
348        // (no credit accumulation), then advance by the byte cost.
349        self.tat
350            .update(Ordering::Relaxed, Ordering::Relaxed, |old| {
351                old.max(now_nanos).saturating_add(weight)
352            });
353    }
354
355    /// Returns `true` if the bucket admits new traffic at `now_nanos`.
356    fn check(&self, now_nanos: u64) -> bool {
357        self.tat.load(Ordering::Relaxed) <= now_nanos.saturating_add(self.burst_ns)
358    }
359}
360
361/// Returns `value * pct / 100`, saturating at `u32::MAX`.
362fn 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
367/// Returns `value * pct / 100`, saturating at `u64::MAX`.
368fn 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 GCRA bucket.
377    global: Option<Arc<BandwidthBucket>>,
378    /// Shared epoch for converting `Instant` to nanos.
379    epoch: Instant,
380    /// Cumulative bytes transferred since startup. Never reset.
381    total_bytes: Arc<AtomicU64>,
382    // NB: These maps grow unbounded but we accept this as we expect an overall limited
383    // number of usecases and scopes. We emit gauge metrics to monitor their size.
384    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    /// Returns nanoseconds elapsed since the shared epoch.
409    fn now_nanos(&self) -> u64 {
410        // u64 nanos overflow after ~584 years of uptime, accepted.
411        self.epoch.elapsed().as_nanos() as u64
412    }
413
414    /// Checks whether the current bandwidth debt exceeds configured limits.
415    ///
416    /// When [`BandwidthLimits::report_only`] is `true`, returns `None` unconditionally.
417    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        // Global check
425        if let Some(ref global) = self.global
426            && !global.check(now_nanos)
427        {
428            return Some(RateLimitRejection::BandwidthGlobal);
429        }
430
431        // Per-usecase check
432        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        // Per-scope check
442        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    /// Returns a handle containing the bandwidth buckets and total-bytes counter.
455    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    /// Returns the effective BPS for per-usecase limiting, if configured.
493    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    /// Returns the effective BPS for per-scope limiting, if configured.
500    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/// Handle for recording bandwidth consumption against GCRA buckets.
508#[derive(Debug, Clone)]
509pub struct BandwidthHandle {
510    buckets: Vec<Arc<BandwidthBucket>>,
511    total_bytes: Arc<AtomicU64>,
512    epoch: Instant,
513}
514
515impl BandwidthHandle {
516    /// Records `bytes` of consumption across all buckets and the total counter.
517    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    /// Cumulative admitted requests since startup. Never reset.
531    total_admitted: Arc<AtomicU64>,
532    // NB: These maps grow unbounded but we accept this as we expect an overall limited
533    // number of usecases and scopes. We emit gauge metrics to monitor their size.
534    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        // NB: We intentionally use unwrap and crash the server if the mutexes are poisoned.
583
584        // Global check
585        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        // Usecase check - only if both global_rps and usecase_pct are set
593        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        // Scope check - only if both global_rps and scope_pct are set
603        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        // Rule checks - each matching rule has its own dedicated bucket
613        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    /// Returns the effective RPS for per-usecase limiting, if configured.
637    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    /// Returns the effective RPS for per-scope limiting, if configured.
644    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    /// Returns the effective RPS for a rule, if it has a valid limit.
651    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/// A token bucket rate limiter.
666///
667/// Tokens refill at a constant rate up to capacity. Each request consumes one token.
668/// When no tokens are available, requests are rejected.
669///
670/// This implementation is not thread-safe on its own. Wrap in a `Mutex` for concurrent access.
671#[derive(Debug)]
672struct TokenBucket {
673    refill_rate: f64,
674    capacity: f64,
675    tokens: f64,
676    last_update: Instant,
677}
678
679impl TokenBucket {
680    /// Creates a new, full token bucket with the specified rate limit and burst capacity.
681    ///
682    /// - `rps`: tokens refilled per second (sustained rate limit)
683    /// - `burst`: initial tokens and burst allowance above sustained rate
684    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    /// Attempts to acquire a token from the bucket.
694    ///
695    /// Returns `true` if a token was acquired, `false` if no tokens available.
696    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        // Only apply refill if we'd gain at least 1 whole token
702        if refilled.floor() > self.tokens.floor() {
703            self.last_update = now;
704            self.tokens = refilled;
705        }
706
707        // Try to consume one token
708        if self.tokens >= 1.0 {
709            self.tokens -= 1.0;
710            true
711        } else {
712            false
713        }
714    }
715}
716
717/// A wrapper around a byte stream that measures bandwidth usage.
718///
719/// Every time a chunk is polled successfully, all bandwidth buckets are charged
720/// and the total-bytes counter is incremented. Generic over both the stream
721/// type `S` and its error type.
722pub(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    // --- BandwidthBucket unit tests (explicit `now`, no sleeps) ---
774
775    #[test]
776    fn bucket_spend_advances_tat() {
777        let bucket = BandwidthBucket::new(1000, 0); // 1000 bps, no burst
778        let now = 1_000_000_000u64; // 1s after epoch
779
780        bucket.spend(now, 500);
781
782        // 500 bytes at 1e6 ns/byte = 500_000_000 ns ahead of `now`
783        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        // Set TAT in the past (idle period).
791        bucket.tat.store(100, Ordering::Relaxed);
792        let now = 2_000_000_000u64;
793
794        bucket.spend(now, 100);
795
796        // TAT should have been clamped to `now` first, then advanced.
797        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); // 1s burst
805        let now = 1_000_000_000u64;
806
807        // Spend 1500 bytes: 1.5s of debt at 1000 bps
808        bucket.spend(now, 1500);
809
810        // debt = 1.5s, burst = 1s → rejected
811        assert!(!bucket.check(now));
812
813        // debt = 1.5s, but 0.6s have passed → debt = 0.9s < 1s burst → admitted
814        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        // Any debt with burst=0 should reject
825        assert!(!bucket.check(now));
826
827        // After enough time passes, debt is paid off
828        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        // 2000 bytes at 1000 bps = 2s of debt
840        assert!(!bucket.check(now));
841        assert!(!bucket.check(now + 1_500_000_000));
842
843        // After 2s, debt is fully paid → admitted
844        assert!(bucket.check(now + 2_000_000_000));
845    }
846
847    #[test]
848    fn bandwidth_check_rejects_correct_variant() {
849        // Use generous burst so only the scope bucket overflows.
850        // Global = 1000 bps, usecase = 500 bps, scope = 250 bps; burst = 2s.
851        // Spending 600 bytes:
852        //   global debt = 600/1000 = 0.6s < 2s → admits
853        //   usecase debt = 600/500 = 1.2s < 2s → admits
854        //   scope debt  = 600/250 = 2.4s > 2s → rejects
855        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    // --- Throughput ---
872
873    #[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        // First call admitted (consumes the one token), second rejected.
899        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        // Put the bandwidth bucket in debt so the check rejects.
920        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}