Skip to main content

rget/
limit.rs

1//! Global bandwidth limiting (PRD §23).
2//!
3//! One token bucket shared by every worker, so `--limit 20MiB/s` means the
4//! download totals 20 MiB/s rather than each of eight connections getting it.
5//! The bucket is a plain mutex + clock rather than a per-worker allowance,
6//! which is what makes the limit global and what will let a future daemon
7//! share one bucket across several downloads.
8
9use std::sync::Mutex;
10use std::time::{Duration, Instant};
11
12pub struct RateLimiter {
13    bytes_per_sec: f64,
14    /// Bucket depth, i.e. the largest burst allowed. A quarter-second of budget
15    /// is enough to absorb the granularity of many workers writing 64 KiB body
16    /// chunks, while keeping the overshoot on a short download small — a full
17    /// second of depth made `--limit 30MiB/s` average 35 MiB/s over 135 MiB,
18    /// which reads as the flag being ignored.
19    capacity: f64,
20    state: Mutex<Bucket>,
21}
22
23struct Bucket {
24    tokens: f64,
25    last: Instant,
26}
27
28/// Never go below this, or a limit smaller than one body chunk would make every
29/// single read block on a sleep.
30const MIN_CAPACITY: f64 = 1.0 * 1024.0 * 1024.0;
31
32impl RateLimiter {
33    pub fn new(bytes_per_sec: u64) -> Self {
34        let rate = bytes_per_sec as f64;
35        // Start full: the alternative is a visible stall at the very start of
36        // every download, which costs more in confusion than the burst does in
37        // accuracy.
38        let capacity = (rate / 4.0).max(MIN_CAPACITY).min(rate.max(1.0));
39        Self {
40            bytes_per_sec: rate,
41            capacity,
42            state: Mutex::new(Bucket {
43                tokens: capacity,
44                last: Instant::now(),
45            }),
46        }
47    }
48
49    /// Block until `n` bytes of budget are available. Called *before* issuing
50    /// the read, so the limit shapes what we pull off the socket rather than
51    /// what we have already buffered.
52    pub async fn acquire(&self, n: u64) {
53        let mut remaining = n as f64;
54        while remaining > 0.0 {
55            let wait = {
56                let mut b = self.state.lock().expect("rate limiter poisoned");
57                let now = Instant::now();
58                let elapsed = now.duration_since(b.last).as_secs_f64();
59                b.last = now;
60                b.tokens = (b.tokens + elapsed * self.bytes_per_sec).min(self.capacity);
61
62                if b.tokens >= remaining {
63                    b.tokens -= remaining;
64                    remaining = 0.0;
65                    Duration::ZERO
66                } else {
67                    // Spend what is there and sleep for the rest. Partial
68                    // spending keeps many waiters progressing fairly instead of
69                    // starving whoever asks for the largest chunk.
70                    remaining -= b.tokens.max(0.0);
71                    b.tokens = 0.0;
72                    Duration::from_secs_f64((remaining / self.bytes_per_sec).min(1.0))
73                }
74            };
75            if wait > Duration::ZERO {
76                tokio::time::sleep(wait).await;
77            }
78        }
79    }
80}
81
82/// Parse `20MiB/s`, `20MB`, `1.5m`, `500k`, `1000` (bytes/s).
83pub fn parse_rate(input: &str) -> Result<u64, String> {
84    let s = input.trim().trim_end_matches("/s").trim_end_matches("/S");
85    let s = s.trim();
86    let split = s
87        .find(|c: char| !c.is_ascii_digit() && c != '.' && c != ',')
88        .unwrap_or(s.len());
89    let (num, unit) = s.split_at(split);
90    let num: f64 = num
91        .replace(',', "")
92        .parse()
93        .map_err(|_| format!("invalid rate `{input}`"))?;
94    if num <= 0.0 {
95        return Err(format!("rate must be positive, got `{input}`"));
96    }
97    let mult: f64 = match unit.trim().to_ascii_lowercase().as_str() {
98        "" | "b" => 1.0,
99        "k" | "kb" | "kib" => 1024.0,
100        "m" | "mb" | "mib" => 1024.0 * 1024.0,
101        "g" | "gb" | "gib" => 1024.0 * 1024.0 * 1024.0,
102        other => return Err(format!("unknown rate unit `{other}` in `{input}`")),
103    };
104    Ok((num * mult) as u64)
105}
106
107/// Parse `30s`, `500ms`, `2m`, `1h`, or a bare number of seconds.
108pub fn parse_duration(input: &str) -> Result<Duration, String> {
109    let s = input.trim();
110    let split = s
111        .find(|c: char| !c.is_ascii_digit() && c != '.')
112        .unwrap_or(s.len());
113    let (num, unit) = s.split_at(split);
114    let num: f64 = num
115        .parse()
116        .map_err(|_| format!("invalid duration `{input}`"))?;
117    let secs = match unit.trim().to_ascii_lowercase().as_str() {
118        "ms" => num / 1000.0,
119        "" | "s" | "sec" | "secs" => num,
120        "m" | "min" | "mins" => num * 60.0,
121        "h" | "hr" | "hrs" => num * 3600.0,
122        other => return Err(format!("unknown duration unit `{other}` in `{input}`")),
123    };
124    if secs <= 0.0 {
125        return Err(format!("duration must be positive, got `{input}`"));
126    }
127    Ok(Duration::from_secs_f64(secs))
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn parses_rates() {
136        assert_eq!(parse_rate("1024"), Ok(1024));
137        assert_eq!(parse_rate("1k"), Ok(1024));
138        assert_eq!(parse_rate("20MiB/s"), Ok(20 * 1024 * 1024));
139        assert_eq!(
140            parse_rate("20 MB / s".replace(' ', "").as_str()),
141            Ok(20 * 1024 * 1024)
142        );
143        assert_eq!(parse_rate("1.5m"), Ok(1_572_864));
144        assert_eq!(parse_rate("2G"), Ok(2 * 1024 * 1024 * 1024));
145        assert!(parse_rate("0").is_err());
146        assert!(parse_rate("-5m").is_err());
147        assert!(parse_rate("fast").is_err());
148        assert!(parse_rate("10furlongs").is_err());
149    }
150
151    #[test]
152    fn parses_durations() {
153        assert_eq!(parse_duration("30"), Ok(Duration::from_secs(30)));
154        assert_eq!(parse_duration("30s"), Ok(Duration::from_secs(30)));
155        assert_eq!(parse_duration("500ms"), Ok(Duration::from_millis(500)));
156        assert_eq!(parse_duration("2m"), Ok(Duration::from_secs(120)));
157        assert_eq!(parse_duration("1h"), Ok(Duration::from_secs(3600)));
158        assert!(parse_duration("0").is_err());
159        assert!(parse_duration("soon").is_err());
160    }
161
162    #[tokio::test(start_paused = true)]
163    async fn limits_throughput_globally() {
164        // A rate well above MIN_CAPACITY, so the burst allowance is rate/4.
165        let rate = 8 * 1024 * 1024;
166        let limiter = RateLimiter::new(rate);
167        let start = tokio::time::Instant::now();
168
169        // The initial burst is free, and it is a quarter-second's worth.
170        limiter.acquire(rate / 4).await;
171        assert_eq!(start.elapsed(), Duration::ZERO);
172
173        // The next 2 seconds' worth must actually take ~2 seconds, no matter how
174        // many callers ask or how they carve it up.
175        limiter.acquire(rate).await;
176        limiter.acquire(rate).await;
177        assert!(
178            start.elapsed() >= Duration::from_millis(1900),
179            "elapsed {:?}",
180            start.elapsed()
181        );
182    }
183
184    #[test]
185    fn burst_allowance_is_a_small_fraction_of_the_rate() {
186        // A quarter second for a fast limit...
187        let fast = RateLimiter::new(40 * 1024 * 1024);
188        assert_eq!(fast.capacity, 10.0 * 1024.0 * 1024.0);
189
190        // ...but never so small that a single body chunk cannot be granted, and
191        // never larger than one second's worth for a very slow limit.
192        let slow = RateLimiter::new(64 * 1024);
193        assert_eq!(slow.capacity, 64.0 * 1024.0);
194        let mid = RateLimiter::new(2 * 1024 * 1024);
195        assert_eq!(mid.capacity, MIN_CAPACITY);
196    }
197}