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//! General capacity limits reject with HTTP 503, while explicit throughput rules
8//! reject with HTTP 429. The first exceeded limit determines the rejection.
9//!
10//! Throughput is enforced using token buckets. Bandwidth uses debt-based GCRA
11//! (Generic Cell Rate Algorithm) buckets that track a theoretical arrival time
12//! (TAT). All rate-limit checks are synchronous, non-blocking, and lock-free.
13
14use 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
32/// Response header that prevents Envoy from retrying a response.
33///
34/// Envoy does not retry a response with this header, even if the route retries on
35/// `gateway-error` or `5xx`. Only the `envoy-ratelimited` retry condition retries it. Outlier
36/// detection still counts the response. See the [Envoy router
37/// documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#x-envoy-ratelimited).
38pub const HEADER_ENVOY_RATELIMITED: &str = "x-envoy-ratelimited";
39
40/// Classifies a rejected request as capacity protection or an exceeded traffic allowance.
41///
42/// This is the single conversion for rate limit and capacity rejections into HTTP responses.
43/// Its [`IntoResponse`] implementation sets the status from [`status`](Self::status) and marks
44/// the response with [`HEADER_ENVOY_RATELIMITED`], so that Envoy does not retry a rejection and
45/// add load to an instance that is already at capacity.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
47pub enum LimitExceeded {
48    /// A general capacity protection limit was exceeded.
49    #[error("service at capacity")]
50    Capacity,
51    /// An explicit traffic allowance was exceeded.
52    #[error("rate limit exceeded")]
53    Allowance,
54}
55
56impl LimitExceeded {
57    /// Returns the HTTP status for capacity protection or an exceeded traffic allowance.
58    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/// Identifies which rate limit triggered a rejection.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum RateLimitRejection {
79    /// Global bandwidth limit exceeded.
80    BandwidthGlobal,
81    /// Per-usecase bandwidth limit exceeded.
82    BandwidthUsecase,
83    /// Per-scope bandwidth limit exceeded.
84    BandwidthScope,
85    /// Global throughput limit exceeded.
86    ThroughputGlobal,
87    /// Per-usecase throughput limit exceeded.
88    ThroughputUsecase,
89    /// Per-scope throughput limit exceeded.
90    ThroughputScope,
91    /// Per-rule throughput limit exceeded.
92    ThroughputRule,
93}
94
95impl RateLimitRejection {
96    /// Classifies the rejection without exposing the specific limit.
97    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    /// Returns a static string identifier suitable for use as a metric tag.
110    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/// Rate limits for objectstore.
130#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
131pub struct RateLimits {
132    /// Limits the number of requests per second per service instance.
133    pub throughput: ThroughputLimits,
134    /// Limits the concurrent bandwidth per service instance.
135    pub bandwidth: BandwidthLimits,
136}
137
138/// Request throughput limits applied at global, per-usecase, and per-scope granularity.
139///
140/// All limits are optional. When a limit is `None`, that level of limiting is not enforced.
141/// Per-usecase and per-scope limits are expressed as a percentage of the global limit.
142///
143/// These general capacity limits return HTTP 503; explicit [`ThroughputRule`]s return HTTP 429.
144#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
145pub struct ThroughputLimits {
146    /// The overall maximum number of requests per second per service instance.
147    ///
148    /// Defaults to `None`, meaning no global rate limit is enforced.
149    pub global_rps: Option<u32>,
150
151    /// The maximum burst for each rate limit.
152    ///
153    /// Defaults to `0`, meaning no bursting is allowed. If set to a value greater than `0`,
154    /// short spikes above the rate limit are allowed up to the burst size.
155    pub burst: u32,
156
157    /// The maximum percentage of the global rate limit that can be used by any usecase.
158    ///
159    /// Value from `0` to `100`. Defaults to `None`, meaning no per-usecase limit is enforced.
160    pub usecase_pct: Option<u8>,
161
162    /// The maximum percentage of the global rate limit that can be used by any scope.
163    ///
164    /// This treats each full scope separately and applies across all use cases:
165    ///  - Two requests with exact same scopes count against the same limit regardless of use case.
166    ///  - Two requests that share the same top scope but differ in inner scopes count separately.
167    ///
168    /// Value from `0` to `100`. Defaults to `None`, meaning no per-scope limit is enforced.
169    pub scope_pct: Option<u8>,
170
171    /// Overrides for specific usecases and scopes.
172    pub rules: Vec<ThroughputRule>,
173}
174
175/// An override rule that applies a specific throughput limit to matching request contexts.
176///
177/// A rule matches when all specified fields match the request context. Fields not set match
178/// any value. When multiple rules match, each is enforced independently via its own token
179/// bucket — all matching rules must admit the request.
180///
181/// Exceeding a rule returns HTTP 429, whether its allowance uses [`Self::rps`] or [`Self::pct`].
182#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
183pub struct ThroughputRule {
184    /// Optional validated usecase name to match exactly.
185    ///
186    /// If `None`, matches any usecase.
187    pub usecase: Option<Usecase>,
188
189    /// Scopes to match.
190    ///
191    /// If empty, matches any scopes. Additional scopes in the context are ignored, so a rule
192    /// matches if all of the specified scopes are present in the request with matching values.
193    pub scopes: Vec<(String, String)>,
194
195    /// The rate limit to apply when this rule matches.
196    ///
197    /// If both a rate and pct are specified, the more restrictive limit applies.
198    /// Should be greater than `0`. To block traffic entirely, use killswitches instead.
199    pub rps: Option<u32>,
200
201    /// The percentage of the global rate limit to apply when this rule matches.
202    ///
203    /// If both a rate and pct are specified, the more restrictive limit applies.
204    /// Should be greater than `0`. To block traffic entirely, use killswitches instead.
205    pub pct: Option<u8>,
206}
207
208impl ThroughputRule {
209    /// Returns `true` if this rule matches the given context.
210    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/// Bandwidth limits applied at global, per-usecase, and per-scope granularity.
229///
230/// Bandwidth is measured as bytes transferred per second (upload + download combined)
231/// and tracked using debt-based GCRA buckets. All limits are optional.
232///
233/// Exceeding an enforced bandwidth limit returns HTTP 503.
234#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
235pub struct BandwidthLimits {
236    /// The overall maximum bandwidth (in bytes per second) per service instance.
237    ///
238    /// Defaults to `None`, meaning no global bandwidth limit is enforced.
239    pub global_bps: Option<u64>,
240
241    /// Burst tolerance in milliseconds.
242    ///
243    /// Allows short traffic spikes up to `burst_ms * bps / 1000` bytes before
244    /// rejection. Defaults to `1000` (1 second).
245    #[serde(default = "default_burst_ms")]
246    pub burst_ms: u64,
247
248    /// The maximum percentage of the global bandwidth limit that can be used by any usecase.
249    ///
250    /// Value from `0` to `100`. Defaults to `None`, meaning no per-usecase bandwidth limit is enforced.
251    pub usecase_pct: Option<u8>,
252
253    /// The maximum percentage of the global bandwidth limit that can be used by any scope.
254    ///
255    /// Value from `0` to `100`. Defaults to `None`, meaning no per-scope bandwidth limit is enforced.
256    pub scope_pct: Option<u8>,
257
258    /// When `true`, bandwidth limits are evaluated and reported but never enforced.
259    ///
260    /// All accounting and metrics remain active, but requests exceeding the limit
261    /// are not rejected. Defaults to `false`.
262    #[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/// Combined rate limiter that enforces both bandwidth and throughput limits.
283///
284/// Checks are synchronous and non-blocking. Bandwidth is checked before
285/// throughput so that rejected requests are never counted toward the admitted
286/// throughput counter. See [`check`](RateLimiter::check) for details.
287///
288/// Call [`start`](RateLimiter::start) after construction to launch the background
289/// observability tasks.
290#[derive(Debug)]
291pub struct RateLimiter {
292    bandwidth: BandwidthRateLimiter,
293    throughput: ThroughputRateLimiter,
294}
295
296impl RateLimiter {
297    /// Creates a new rate limiter from the given configuration.
298    ///
299    /// Background observability tasks are not started until [`start`](RateLimiter::start) is called.
300    pub fn new(config: RateLimits) -> Self {
301        Self {
302            bandwidth: BandwidthRateLimiter::new(config.bandwidth),
303            throughput: ThroughputRateLimiter::new(config.throughput),
304        }
305    }
306
307    /// Starts background tasks for rate limit monitoring.
308    ///
309    /// Must be called from within a Tokio runtime.
310    pub fn start(&self) {
311        self.throughput.start();
312    }
313
314    /// Checks if the given context is within the rate limits.
315    ///
316    /// Returns a capacity or allowance error on rejection, emitting the detailed reason in a
317    /// `server.request.rate_limited` metric counter and a `warn!` log. Bandwidth is checked
318    /// before throughput, then global, usecase, scope, and explicit throughput rules are checked
319    /// in order. General limits take precedence over explicit rules.
320    pub fn check(&self, context: &ObjectContext, key: Option<&str>) -> Result<(), LimitExceeded> {
321        // Bandwidth is checked first because it is a pure read (no token consumption).
322        // Throughput counts admitted requests, so checking it second ensures rejected
323        // requests are never counted toward admitted traffic.
324        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    /// Returns bandwidth buckets and the total-bytes counter for the given context.
349    ///
350    /// Creates entries in the per-usecase/per-scope maps if they don't exist yet.
351    pub fn bandwidth_handle(&self, context: &ObjectContext) -> BandwidthHandle {
352        self.bandwidth.handle(context)
353    }
354
355    /// Records bandwidth usage across all buckets for the given context.
356    ///
357    /// This is used for cases where bytes are known upfront (e.g. batch INSERT) rather than
358    /// streamed through a `MeteredPayloadStream`.
359    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    /// Returns the configured global bandwidth limit in bytes/s, if set.
365    pub fn bandwidth_limit(&self) -> Option<u64> {
366        self.bandwidth.config.global_bps
367    }
368
369    /// Returns the configured global throughput limit in requests/s, if set.
370    pub fn throughput_limit(&self) -> Option<u32> {
371        self.throughput.config.global_rps
372    }
373
374    /// Returns total bytes transferred since startup.
375    pub fn bandwidth_total_bytes(&self) -> u64 {
376        self.bandwidth.total_bytes.load(Ordering::Relaxed)
377    }
378
379    /// Returns total admitted requests since startup.
380    pub fn throughput_total_admitted(&self) -> u64 {
381        self.throughput.total_admitted.load(Ordering::Relaxed)
382    }
383}
384
385/// Debt-based GCRA bandwidth bucket.
386///
387/// Tracks a theoretical arrival time (TAT) that advances by `nanos_per_byte`
388/// for every byte consumed. Admission is granted while TAT stays within
389/// `burst_ns` nanoseconds ahead of the wall clock (burst tolerance). Recovery is
390/// continuous — no background tick required.
391#[derive(Debug)]
392struct BandwidthBucket {
393    /// Nanoseconds since `epoch` at which all recorded traffic is paid off.
394    ///
395    /// Saturates at `u64::MAX` after ~584 years of uptime.
396    tat: AtomicU64,
397    /// Nanoseconds of TAT advance per byte: `1e9 / bps`.
398    nanos_per_byte: f64,
399    /// Burst tolerance in nanoseconds: `burst_ms * 1_000_000`.
400    burst_ns: u64,
401}
402
403impl BandwidthBucket {
404    /// Creates a new bucket for the given rate and burst tolerance.
405    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    /// Records `bytes` of consumption unconditionally (debt is allowed).
416    fn spend(&self, now_nanos: u64, bytes: u64) {
417        let weight = (bytes as f64 * self.nanos_per_byte) as u64;
418        // Clamp + advance in one atomic step: never let TAT fall behind `now`
419        // (no credit accumulation), then advance by the byte cost.
420        self.tat
421            .update(Ordering::Relaxed, Ordering::Relaxed, |old| {
422                old.max(now_nanos).saturating_add(weight)
423            });
424    }
425
426    /// Returns `true` if the bucket admits new traffic at `now_nanos`.
427    fn check(&self, now_nanos: u64) -> bool {
428        self.tat.load(Ordering::Relaxed) <= now_nanos.saturating_add(self.burst_ns)
429    }
430}
431
432/// Returns `value * pct / 100`, saturating at `u32::MAX`.
433fn 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
438/// Returns `value * pct / 100`, saturating at `u64::MAX`.
439fn 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 GCRA bucket.
448    global: Option<Arc<BandwidthBucket>>,
449    /// Shared epoch for converting `Instant` to nanos.
450    epoch: Instant,
451    /// Cumulative bytes transferred since startup. Never reset.
452    total_bytes: Arc<AtomicU64>,
453    // NB: These maps grow unbounded but we accept this as we expect an overall limited
454    // number of usecases and scopes. We emit gauge metrics to monitor their size.
455    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    /// Returns nanoseconds elapsed since the shared epoch.
480    fn now_nanos(&self) -> u64 {
481        // u64 nanos overflow after ~584 years of uptime, accepted.
482        self.epoch.elapsed().as_nanos() as u64
483    }
484
485    /// Checks whether the current bandwidth debt exceeds configured limits.
486    ///
487    /// When [`BandwidthLimits::report_only`] is `true`, returns `None` unconditionally.
488    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        // Global check
496        if let Some(ref global) = self.global
497            && !global.check(now_nanos)
498        {
499            return Some(RateLimitRejection::BandwidthGlobal);
500        }
501
502        // Per-usecase check
503        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        // Per-scope check
513        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    /// Returns a handle containing the bandwidth buckets and total-bytes counter.
526    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    /// Returns the effective BPS for per-usecase limiting, if configured.
564    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    /// Returns the effective BPS for per-scope limiting, if configured.
571    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/// Handle for recording bandwidth consumption against GCRA buckets.
579#[derive(Debug, Clone)]
580pub struct BandwidthHandle {
581    buckets: Vec<Arc<BandwidthBucket>>,
582    total_bytes: Arc<AtomicU64>,
583    epoch: Instant,
584}
585
586impl BandwidthHandle {
587    /// Records `bytes` of consumption across all buckets and the total counter.
588    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    /// Cumulative admitted requests since startup. Never reset.
602    total_admitted: Arc<AtomicU64>,
603    // NB: These maps grow unbounded but we accept this as we expect an overall limited
604    // number of usecases and scopes. We emit gauge metrics to monitor their size.
605    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        // NB: We intentionally use unwrap and crash the server if the mutexes are poisoned.
654
655        // Global check
656        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        // Usecase check - only if both global_rps and usecase_pct are set
664        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        // Scope check - only if both global_rps and scope_pct are set
674        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        // Rule checks - each matching rule has its own dedicated bucket
684        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    /// Returns the effective RPS for per-usecase limiting, if configured.
708    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    /// Returns the effective RPS for per-scope limiting, if configured.
715    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    /// Returns the effective RPS for a rule, if it has a valid limit.
722    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/// A token bucket rate limiter.
737///
738/// Tokens refill at a constant rate up to capacity. Each request consumes one token.
739/// When no tokens are available, requests are rejected.
740///
741/// This implementation is not thread-safe on its own. Wrap in a `Mutex` for concurrent access.
742#[derive(Debug)]
743struct TokenBucket {
744    refill_rate: f64,
745    capacity: f64,
746    tokens: f64,
747    last_update: Instant,
748}
749
750impl TokenBucket {
751    /// Creates a new, full token bucket with the specified rate limit and burst capacity.
752    ///
753    /// - `rps`: tokens refilled per second (sustained rate limit)
754    /// - `burst`: initial tokens and burst allowance above sustained rate
755    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    /// Attempts to acquire a token from the bucket.
765    ///
766    /// Returns `true` if a token was acquired, `false` if no tokens available.
767    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        // Only apply refill if we'd gain at least 1 whole token
773        if refilled.floor() > self.tokens.floor() {
774            self.last_update = now;
775            self.tokens = refilled;
776        }
777
778        // Try to consume one token
779        if self.tokens >= 1.0 {
780            self.tokens -= 1.0;
781            true
782        } else {
783            false
784        }
785    }
786}
787
788/// A wrapper around a byte stream that measures bandwidth usage.
789///
790/// Every time a chunk is polled successfully, all bandwidth buckets are charged
791/// and the total-bytes counter is incremented. Generic over both the stream
792/// type `S` and its error type.
793pub(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    // --- BandwidthBucket unit tests (explicit `now`, no sleeps) ---
872
873    #[test]
874    fn bucket_spend_advances_tat() {
875        let bucket = BandwidthBucket::new(1000, 0); // 1000 bps, no burst
876        let now = 1_000_000_000u64; // 1s after epoch
877
878        bucket.spend(now, 500);
879
880        // 500 bytes at 1e6 ns/byte = 500_000_000 ns ahead of `now`
881        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        // Set TAT in the past (idle period).
889        bucket.tat.store(100, Ordering::Relaxed);
890        let now = 2_000_000_000u64;
891
892        bucket.spend(now, 100);
893
894        // TAT should have been clamped to `now` first, then advanced.
895        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); // 1s burst
903        let now = 1_000_000_000u64;
904
905        // Spend 1500 bytes: 1.5s of debt at 1000 bps
906        bucket.spend(now, 1500);
907
908        // debt = 1.5s, burst = 1s → rejected
909        assert!(!bucket.check(now));
910
911        // debt = 1.5s, but 0.6s have passed → debt = 0.9s < 1s burst → admitted
912        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        // Any debt with burst=0 should reject
923        assert!(!bucket.check(now));
924
925        // After enough time passes, debt is paid off
926        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        // 2000 bytes at 1000 bps = 2s of debt
938        assert!(!bucket.check(now));
939        assert!(!bucket.check(now + 1_500_000_000));
940
941        // After 2s, debt is fully paid → admitted
942        assert!(bucket.check(now + 2_000_000_000));
943    }
944
945    #[test]
946    fn bandwidth_check_rejects_correct_variant() {
947        // Use generous burst so only the scope bucket overflows.
948        // Global = 1000 bps, usecase = 500 bps, scope = 250 bps; burst = 2s.
949        // Spending 600 bytes:
950        //   global debt = 600/1000 = 0.6s < 2s → admits
951        //   usecase debt = 600/500 = 1.2s < 2s → admits
952        //   scope debt  = 600/250 = 2.4s > 2s → rejects
953        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    // --- Throughput ---
970
971    #[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        // First call admitted (consumes the one token), second rejected.
997        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        // Put the bandwidth bucket in debt so the check rejects.
1018        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}