Skip to main content

aria2_core/
rate_limiter.rs

1use async_trait::async_trait;
2use std::fmt;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::time::{Duration, Instant};
6
7use tracing::{debug, warn};
8
9use crate::constants;
10use crate::error::Result;
11use crate::filesystem::disk_writer::DiskWriter;
12
13#[derive(Clone, Debug, Default)]
14pub struct RateLimiterConfig {
15    pub max_download_bytes_per_sec: Option<u64>,
16    pub max_upload_bytes_per_sec: Option<u64>,
17    pub download_burst_bytes: Option<u64>,
18    pub upload_burst_bytes: Option<u64>,
19}
20
21impl RateLimiterConfig {
22    pub fn new(download_limit: Option<u64>, upload_limit: Option<u64>) -> Self {
23        Self {
24            max_download_bytes_per_sec: download_limit,
25            max_upload_bytes_per_sec: upload_limit,
26            download_burst_bytes: None,
27            upload_burst_bytes: None,
28        }
29    }
30
31    pub fn with_burst(mut self, download_burst: Option<u64>, upload_burst: Option<u64>) -> Self {
32        self.download_burst_bytes = download_burst;
33        self.upload_burst_bytes = upload_burst;
34        self
35    }
36
37    pub fn is_limited(&self) -> bool {
38        self.max_download_bytes_per_sec.is_some() || self.max_upload_bytes_per_sec.is_some()
39    }
40
41    pub fn download_rate(&self) -> Option<u64> {
42        self.max_download_bytes_per_sec
43    }
44
45    pub fn upload_rate(&self) -> Option<u64> {
46        self.max_upload_bytes_per_sec
47    }
48
49    pub fn download_burst(&self) -> Option<u64> {
50        self.download_burst_bytes
51    }
52
53    pub fn upload_burst(&self) -> Option<u64> {
54        self.upload_burst_bytes
55    }
56}
57
58/// Nanoseconds per second — used for integer time/rate conversions.
59const NS_PER_SEC: u64 = 1_000_000_000;
60
61/// Minimum wait duration before issuing a `tokio::time::sleep`.
62/// Waits shorter than this use a spin-loop hint instead to avoid
63/// the scheduling overhead of waking a task for sub-microsecond delays.
64const MIN_SLEEP: Duration = Duration::from_micros(1);
65
66/// Lock-free token bucket using atomic CAS operations.
67///
68/// All mutable state is stored in `AtomicU64` — no `Mutex` is acquired on the
69/// hot path. Token refill is computed lazily on each `acquire` / `try_acquire`
70/// call based on elapsed time since the last refill.
71///
72/// Integer arithmetic is used throughout (no `f64`) for deterministic behaviour
73/// and to avoid floating-point CAS issues. Token counts are tracked in
74/// **milli-tokens** (tokens * 1000) to provide sub-token precision while
75/// staying in integer domain.
76///
77/// All public methods take `&self` (not `&mut self`), enabling concurrent
78/// access from multiple tasks via a shared reference.
79pub struct TokenBucket {
80    /// Current token count in milli-tokens (tokens * 1000).
81    /// Updated via CAS — never read-modify-write without compare_exchange.
82    tokens_milli: AtomicU64,
83    /// Maximum capacity in milli-tokens. Immutable after construction.
84    capacity_milli: u64,
85    /// Refill rate in milli-tokens per second. Mutable via `set_rate` for
86    /// dynamic rate adjustment.
87    /// `rate_milli_per_sec = rate_bytes_per_sec * 1000`.
88    rate_milli_per_sec: AtomicU64,
89    /// Last refill timestamp — nanoseconds elapsed since `anchor`.
90    /// Updated via CAS to claim a refill slot (only the winning thread adds tokens).
91    last_refill_elapsed_ns: AtomicU64,
92    /// Whether this bucket is unlimited (rate = infinity). Mutable via
93    /// `set_unlimited` for dynamic mode switching.
94    unlimited: AtomicBool,
95    /// Anchor `Instant` created at construction; used to compute elapsed nanoseconds.
96    /// Never mutated — `Instant` is `Send + Sync`.
97    anchor: Instant,
98}
99
100impl fmt::Debug for TokenBucket {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("TokenBucket")
103            .field("tokens_milli", &self.tokens_milli.load(Ordering::Relaxed))
104            .field("capacity_milli", &self.capacity_milli)
105            .field(
106                "rate_milli_per_sec",
107                &self.rate_milli_per_sec.load(Ordering::Relaxed),
108            )
109            .field("unlimited", &self.unlimited.load(Ordering::Relaxed))
110            .finish()
111    }
112}
113
114impl TokenBucket {
115    /// Create a new token bucket with the given rate and optional burst.
116    ///
117    /// `rate_bytes_per_sec` of 0 produces a bucket that never refills — callers
118    /// should use [`TokenBucket::unlimited`] instead for "no limit" semantics.
119    pub fn new(rate_bytes_per_sec: u64, burst_bytes: Option<u64>) -> Self {
120        let burst = burst_bytes.unwrap_or(constants::DEFAULT_BURST_BYTES as u64);
121        let anchor = Instant::now();
122        Self {
123            tokens_milli: AtomicU64::new(burst.saturating_mul(1000)),
124            capacity_milli: burst.saturating_mul(1000),
125            rate_milli_per_sec: AtomicU64::new(rate_bytes_per_sec.saturating_mul(1000)),
126            last_refill_elapsed_ns: AtomicU64::new(0),
127            unlimited: AtomicBool::new(false),
128            anchor,
129        }
130    }
131
132    /// Create an unlimited token bucket — `acquire` / `try_acquire` always
133    /// succeed instantly without consuming any real tokens.
134    pub fn unlimited() -> Self {
135        let anchor = Instant::now();
136        // Use a large but safe value to avoid overflow on arithmetic.
137        let huge = u64::MAX / 4;
138        Self {
139            tokens_milli: AtomicU64::new(huge),
140            capacity_milli: huge,
141            rate_milli_per_sec: AtomicU64::new(huge),
142            last_refill_elapsed_ns: AtomicU64::new(0),
143            unlimited: AtomicBool::new(true),
144            anchor,
145        }
146    }
147
148    /// Returns `true` if this bucket has no rate limit.
149    pub fn is_unlimited(&self) -> bool {
150        self.unlimited.load(Ordering::Relaxed)
151    }
152
153    /// Returns the configured rate in bytes per second (as `f64` for API compat).
154    /// Returns `f64::MAX` for unlimited buckets.
155    pub fn rate(&self) -> f64 {
156        if self.unlimited.load(Ordering::Relaxed) {
157            f64::MAX
158        } else {
159            self.rate_milli_per_sec.load(Ordering::Relaxed) as f64 / 1000.0
160        }
161    }
162
163    /// Returns the current available tokens (as `f64` for API compat).
164    /// Triggers a lazy refill before reading.
165    /// Returns `f64::MAX` for unlimited buckets.
166    pub fn available_tokens(&self) -> f64 {
167        if self.unlimited.load(Ordering::Relaxed) {
168            return f64::MAX;
169        }
170        self.refill();
171        self.tokens_milli.load(Ordering::Relaxed) as f64 / 1000.0
172    }
173
174    /// Nanoseconds elapsed since the anchor `Instant`.
175    #[inline]
176    fn now_ns(&self) -> u64 {
177        // saturating_duration_since avoids panic on clock anomalies.
178        // now - anchor = elapsed time since construction.
179        Instant::now()
180            .saturating_duration_since(self.anchor)
181            .as_nanos() as u64
182    }
183
184    /// Lazily refill tokens based on elapsed time since the last refill.
185    ///
186    /// Uses a **CAS-claim** pattern: only the thread that successfully advances
187    /// `last_refill_elapsed_ns` adds tokens. This prevents double-counting when
188    /// multiple threads call `refill` concurrently.
189    ///
190    /// Formula: `added_milli = elapsed_ns * rate_milli_per_sec / NS_PER_SEC`
191    /// (the 1000× from milli-tokens cancels with the 1000× in rate_milli_per_sec).
192    fn refill(&self) {
193        if self.unlimited.load(Ordering::Relaxed) {
194            return;
195        }
196        let now = self.now_ns();
197        let last = self.last_refill_elapsed_ns.load(Ordering::Relaxed);
198        if now <= last {
199            // No time elapsed since last refill (or clock went backwards).
200            return;
201        }
202        let elapsed_ns = now - last;
203        // u128 to avoid overflow: elapsed_ns (u64) * rate_milli_per_sec (u64).
204        let added_milli = ((elapsed_ns as u128)
205            * (self.rate_milli_per_sec.load(Ordering::Relaxed) as u128)
206            / NS_PER_SEC as u128) as u64;
207        if added_milli == 0 {
208            // Less than 1 milli-token elapsed — do NOT advance last_refill to
209            // preserve fractional accumulation for the next call.
210            return;
211        }
212        // Claim the refill: only the winner of this CAS proceeds to add tokens.
213        // Losers abort — another thread already refilled for a overlapping period.
214        match self.last_refill_elapsed_ns.compare_exchange(
215            last,
216            now,
217            Ordering::Relaxed,
218            Ordering::Relaxed,
219        ) {
220            Ok(_) => {
221                // Won the claim — add tokens, capping at capacity.
222                loop {
223                    let current = self.tokens_milli.load(Ordering::Relaxed);
224                    let new = current.saturating_add(added_milli).min(self.capacity_milli);
225                    match self.tokens_milli.compare_exchange_weak(
226                        current,
227                        new,
228                        Ordering::Relaxed,
229                        Ordering::Relaxed,
230                    ) {
231                        Ok(_) => break,
232                        Err(_) => continue, // Another thread modified tokens — retry.
233                    }
234                }
235            }
236            Err(_) => {
237                // Lost the claim — another thread already refilled. Nothing to do.
238            }
239        }
240    }
241
242    /// Acquire `bytes` tokens, blocking (async-sleeping) until enough tokens
243    /// are available.
244    ///
245    /// For requests larger than the burst capacity, this method waits for the
246    /// deficit and then force-acquires (setting tokens to 0), matching the
247    /// original implementation's behaviour of allowing token "debt" clamped to
248    /// zero. This prevents infinite loops when `needed > capacity`.
249    pub async fn acquire(&self, bytes: u64) {
250        if self.unlimited.load(Ordering::Relaxed) {
251            return;
252        }
253        // milli-tokens needed; saturating_mul caps at u64::MAX on overflow.
254        let needed_milli = bytes.saturating_mul(1000);
255
256        loop {
257            self.refill();
258            let current = self.tokens_milli.load(Ordering::Relaxed);
259            if current >= needed_milli {
260                // Enough tokens — try CAS to deduct.
261                match self.tokens_milli.compare_exchange_weak(
262                    current,
263                    current - needed_milli,
264                    Ordering::Relaxed,
265                    Ordering::Relaxed,
266                ) {
267                    Ok(_) => return,
268                    Err(_) => continue, // Raced — retry.
269                }
270            }
271
272            // Not enough tokens — compute wait time from the deficit.
273            let deficit_milli = needed_milli - current;
274            let rate_milli = self.rate_milli_per_sec.load(Ordering::Relaxed);
275            if rate_milli == 0 {
276                // Rate is 0 — would wait forever. Defensively treat as unlimited
277                // rather than hanging the caller.
278                warn!("TokenBucket::acquire with rate=0; treating as unlimited");
279                return;
280            }
281            // wait_ns = deficit_milli * NS_PER_SEC / rate_milli_per_sec
282            // (u128 to avoid overflow).
283            let wait_ns =
284                ((deficit_milli as u128) * NS_PER_SEC as u128 / rate_milli as u128) as u64;
285            let wait = Duration::from_nanos(wait_ns);
286
287            if wait < MIN_SLEEP {
288                // Very short wait — spin instead of paying scheduler overhead.
289                std::hint::spin_loop();
290                continue;
291            }
292
293            debug!(
294                bytes = bytes,
295                deficit_milli = deficit_milli,
296                wait_ns = wait_ns,
297                "throttling: sleeping for token refill"
298            );
299            tokio::time::sleep(wait).await;
300
301            // After sleeping, force-acquire: refill, then deduct (clamped to 0).
302            // This matches the original behaviour where tokens can go negative
303            // (clamped to 0) when the request exceeds burst capacity.
304            self.refill();
305            loop {
306                let cur = self.tokens_milli.load(Ordering::Relaxed);
307                let new = cur.saturating_sub(needed_milli);
308                match self.tokens_milli.compare_exchange_weak(
309                    cur,
310                    new,
311                    Ordering::Relaxed,
312                    Ordering::Relaxed,
313                ) {
314                    Ok(_) => return,
315                    Err(_) => continue,
316                }
317            }
318        }
319    }
320
321    /// Non-blocking attempt to acquire `bytes` tokens.
322    /// Returns `true` if tokens were available and deducted, `false` otherwise.
323    pub fn try_acquire(&self, bytes: u64) -> bool {
324        if self.unlimited.load(Ordering::Relaxed) {
325            return true;
326        }
327        self.refill();
328        let needed_milli = bytes.saturating_mul(1000);
329        loop {
330            let current = self.tokens_milli.load(Ordering::Relaxed);
331            if current < needed_milli {
332                return false;
333            }
334            match self.tokens_milli.compare_exchange_weak(
335                current,
336                current - needed_milli,
337                Ordering::Relaxed,
338                Ordering::Relaxed,
339            ) {
340                Ok(_) => return true,
341                Err(_) => continue,
342            }
343        }
344    }
345
346    /// Update the refill rate dynamically. Takes effect on the next refill cycle.
347    /// `rate_bytes_per_sec` of 0 effectively pauses the bucket (no new tokens).
348    pub fn set_rate(&self, rate_bytes_per_sec: u64) {
349        self.rate_milli_per_sec
350            .store(rate_bytes_per_sec.saturating_mul(1000), Ordering::Relaxed);
351    }
352
353    /// Toggle the unlimited flag. When set to true, acquire/try_acquire always
354    /// succeed instantly without consuming tokens.
355    pub fn set_unlimited(&self, unlimited: bool) {
356        self.unlimited.store(unlimited, Ordering::Relaxed);
357    }
358}
359
360/// Inner state of `RateLimiter`, shared via `Arc` so that cloning a
361/// `RateLimiter` shares the same token buckets and limit flags (no Mutex
362/// involved). The `download_limited` / `upload_limited` flags live here so
363/// that `set_download_rate` / `set_upload_rate` on one clone are visible to
364/// all clones.
365struct RateLimiterInner {
366    download: TokenBucket,
367    upload: TokenBucket,
368    download_limited: AtomicBool,
369    upload_limited: AtomicBool,
370}
371
372/// Rate limiter for download and upload bandwidth.
373///
374/// Cloning a `RateLimiter` shares the underlying token buckets — all clones
375/// draw from the same pool. The hot path (`acquire_download` / `acquire_upload`)
376/// performs **no mutex acquisition**: token accounting is done entirely via
377/// atomic CAS operations on the inner `TokenBucket`s.
378#[derive(Clone)]
379pub struct RateLimiter {
380    inner: Arc<RateLimiterInner>,
381}
382
383impl RateLimiter {
384    pub fn new(config: &RateLimiterConfig) -> Self {
385        let dl_rate = config.download_rate();
386        let ul_rate = config.upload_rate();
387        let dl_burst = config.download_burst();
388        let ul_burst = config.upload_burst();
389
390        let download = match dl_rate {
391            Some(rate) if rate > 0 => TokenBucket::new(rate, dl_burst),
392            _ => TokenBucket::unlimited(),
393        };
394        let upload = match ul_rate {
395            Some(rate) if rate > 0 => TokenBucket::new(rate, ul_burst),
396            _ => TokenBucket::unlimited(),
397        };
398
399        Self {
400            inner: Arc::new(RateLimiterInner {
401                download,
402                upload,
403                download_limited: AtomicBool::new(dl_rate.is_some_and(|r| r > 0)),
404                upload_limited: AtomicBool::new(ul_rate.is_some_and(|r| r > 0)),
405            }),
406        }
407    }
408
409    pub fn unlimited() -> Self {
410        Self::new(&RateLimiterConfig::default())
411    }
412
413    pub async fn acquire_download(&self, bytes: u64) {
414        self.inner.download.acquire(bytes).await;
415    }
416
417    pub async fn acquire_upload(&self, bytes: u64) {
418        self.inner.upload.acquire(bytes).await;
419    }
420
421    /// Non-blocking attempt to acquire download tokens.
422    /// Returns `true` if tokens were available, `false` otherwise (no wait).
423    #[allow(clippy::unused_async)]
424    pub async fn try_acquire_download(&self, bytes: u64) -> bool {
425        self.inner.download.try_acquire(bytes)
426    }
427
428    /// Non-blocking attempt to acquire upload tokens.
429    /// Returns `true` if tokens were available, `false` otherwise (no wait).
430    #[allow(clippy::unused_async)]
431    pub async fn try_acquire_upload(&self, bytes: u64) -> bool {
432        self.inner.upload.try_acquire(bytes)
433    }
434
435    pub fn is_download_limited(&self) -> bool {
436        self.inner.download_limited.load(Ordering::Relaxed)
437    }
438
439    pub fn is_upload_limited(&self) -> bool {
440        self.inner.upload_limited.load(Ordering::Relaxed)
441    }
442
443    pub async fn config(&self) -> RateLimiterConfig {
444        RateLimiterConfig::new(
445            if self.inner.download.is_unlimited() {
446                None
447            } else {
448                Some(self.inner.download.rate() as u64)
449            },
450            if self.inner.upload.is_unlimited() {
451                None
452            } else {
453                Some(self.inner.upload.rate() as u64)
454            },
455        )
456    }
457
458    /// Dynamically update the download rate limit.
459    /// `None` or `Some(0)` means unlimited (no throttling).
460    /// `Some(rate)` where rate > 0 sets the new rate in bytes/sec.
461    pub fn set_download_rate(&self, rate: Option<u64>) {
462        match rate {
463            Some(r) if r > 0 => {
464                self.inner.download.set_unlimited(false);
465                self.inner.download.set_rate(r);
466                self.inner.download_limited.store(true, Ordering::Relaxed);
467            }
468            _ => {
469                self.inner.download.set_unlimited(true);
470                self.inner.download_limited.store(false, Ordering::Relaxed);
471            }
472        }
473    }
474
475    /// Dynamically update the upload rate limit.
476    /// Same semantics as `set_download_rate`.
477    pub fn set_upload_rate(&self, rate: Option<u64>) {
478        match rate {
479            Some(r) if r > 0 => {
480                self.inner.upload.set_unlimited(false);
481                self.inner.upload.set_rate(r);
482                self.inner.upload_limited.store(true, Ordering::Relaxed);
483            }
484            _ => {
485                self.inner.upload.set_unlimited(true);
486                self.inner.upload_limited.store(false, Ordering::Relaxed);
487            }
488        }
489    }
490}
491
492/// A `DiskWriter` wrapper that throttles writes via a `RateLimiter`.
493///
494/// Token acquisition is **batched**: a single `write()` call acquires tokens
495/// for the entire buffer upfront (one CAS sequence), then writes the data in
496/// chunks to the inner writer. This avoids per-chunk lock contention — at
497/// 1 GiB/s with 8 KB chunks that is 125 000 fewer acquire calls per second.
498pub struct ThrottledWriter<W> {
499    inner: W,
500    limiter: RateLimiter,
501    chunk_size: usize,
502}
503
504impl<W> ThrottledWriter<W>
505where
506    W: DiskWriter + Send,
507{
508    pub fn new(inner: W, limiter: RateLimiter) -> Self {
509        Self {
510            inner,
511            limiter,
512            chunk_size: constants::RATE_LIMITER_CHUNK_SIZE,
513        }
514    }
515
516    pub fn with_chunk_size(mut self, size: usize) -> Self {
517        self.chunk_size = size.max(constants::RATE_LIMITER_MIN_CHUNK_SIZE);
518        self
519    }
520
521    pub fn into_inner(self) -> W {
522        self.inner
523    }
524
525    pub fn limiter(&self) -> &RateLimiter {
526        &self.limiter
527    }
528}
529
530#[async_trait]
531impl<W> DiskWriter for ThrottledWriter<W>
532where
533    W: DiskWriter + Send,
534{
535    async fn write(&mut self, data: &[u8]) -> Result<()> {
536        if !self.limiter.is_download_limited() {
537            return self.inner.write(data).await;
538        }
539
540        // Acquire tokens per-chunk (not batched for the entire buffer).
541        //
542        // Rationale: reqwest's `bytes_stream()` yields chunks whose sizes grow
543        // adaptively (8K → 16K → 32K → … → 256K+) on fast links. A single
544        // batched `acquire_download(entire_buffer)` for a 417 KB chunk at
545        // 80 KB/s would sleep for ~5.2 s. That sleep is a fixed
546        // `tokio::time::sleep` and is NOT interrupted when `changeOption`
547        // updates the rate mid-sleep, making dynamic rate changes appear to
548        // stall the download.
549        //
550        // Per-chunk acquisition bounds each `acquire` to
551        // `chunk_size / rate` seconds (e.g. 8 KB / 80 KB/s = 0.1 s), so a
552        // rate change takes effect within at most one chunk's duration. The
553        // lock-free CAS in `TokenBucket::acquire` keeps overhead negligible
554        // even at high rates where `try_acquire`-style fast paths trigger.
555        if data.len() <= self.chunk_size {
556            self.limiter.acquire_download(data.len() as u64).await;
557            return self.inner.write(data).await;
558        }
559
560        let mut offset = 0usize;
561        while offset < data.len() {
562            let end = (offset + self.chunk_size).min(data.len());
563            let chunk = &data[offset..end];
564            self.limiter.acquire_download(chunk.len() as u64).await;
565            self.inner.write(chunk).await?;
566            offset = end;
567        }
568        Ok(())
569    }
570
571    async fn finalize(&mut self) -> Result<Vec<u8>> {
572        self.inner.finalize().await
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use std::sync::Arc;
580
581    #[tokio::test]
582    async fn test_token_bucket_unlimited() {
583        let tb = TokenBucket::unlimited();
584        assert!(tb.is_unlimited());
585        tb.acquire(1024 * 1024 * 1024).await;
586        assert!(tb.available_tokens() > 0.0);
587    }
588
589    #[tokio::test]
590    async fn test_token_bucket_basic_acquire() {
591        let tb = TokenBucket::new(10000, Some(5000));
592        assert!(!tb.is_unlimited());
593
594        let start = Instant::now();
595        tb.acquire(5000).await;
596        let elapsed = start.elapsed();
597        assert!(
598            elapsed < Duration::from_millis(100),
599            "burst should be instant: {:?}",
600            elapsed
601        );
602
603        tb.acquire(6000).await;
604        let total_elapsed = start.elapsed();
605        let expected_min = Duration::from_millis(100);
606        assert!(
607            total_elapsed >= expected_min.saturating_sub(Duration::from_millis(200)),
608            "should have waited for refill: got {:?} expected >= {:?}",
609            total_elapsed,
610            expected_min
611        );
612    }
613
614    #[tokio::test]
615    async fn test_token_bucket_try_acquire() {
616        let tb = TokenBucket::new(1000, Some(2000));
617
618        assert!(tb.try_acquire(1000));
619        assert!(tb.try_acquire(1000));
620        assert!(!tb.try_acquire(1));
621    }
622
623    #[test]
624    fn test_token_bucket_available_tokens() {
625        let tb = TokenBucket::new(1000, Some(5000));
626        let initial = tb.available_tokens();
627        assert!(
628            (initial - 5000.0).abs() < 0.01,
629            "initial tokens should be ~5000, got {}",
630            initial
631        );
632
633        tb.try_acquire(2000);
634        let after = tb.available_tokens();
635        assert!(
636            (after - 3000.0).abs() < 0.01,
637            "after acquiring 2000, should have ~3000, got {}",
638            after
639        );
640    }
641
642    #[test]
643    fn test_rate_limiter_config_default() {
644        let cfg = RateLimiterConfig::default();
645        assert!(!cfg.is_limited());
646        assert!(cfg.download_rate().is_none());
647        assert!(cfg.upload_rate().is_none());
648    }
649
650    #[test]
651    fn test_rate_limiter_config_new() {
652        let cfg = RateLimiterConfig::new(Some(1024), Some(512));
653        assert!(cfg.is_limited());
654        assert_eq!(cfg.download_rate(), Some(1024));
655        assert_eq!(cfg.upload_rate(), Some(512));
656    }
657
658    #[test]
659    fn test_rate_limiter_config_download_only() {
660        let cfg = RateLimiterConfig::new(Some(2048), None);
661        assert!(cfg.is_limited());
662        assert_eq!(cfg.download_rate(), Some(2048));
663        assert!(cfg.upload_rate().is_none());
664    }
665
666    #[tokio::test]
667    async fn test_rate_limiter_unlimited() {
668        let rl = RateLimiter::unlimited();
669        assert!(!rl.is_download_limited());
670        assert!(!rl.is_upload_limited());
671        rl.acquire_download(999999).await;
672        rl.acquire_upload(999999).await;
673    }
674
675    #[tokio::test]
676    async fn test_rate_limiter_with_limits() {
677        let cfg = RateLimiterConfig::new(Some(5000), Some(1000)).with_burst(Some(1000), Some(500));
678        let rl = RateLimiter::new(&cfg);
679        assert!(rl.is_download_limited());
680        assert!(rl.is_upload_limited());
681
682        let start = Instant::now();
683        rl.acquire_download(6000).await;
684        let elapsed = start.elapsed();
685        assert!(
686            elapsed >= Duration::from_millis(800),
687            "should throttle: got {:?}",
688            elapsed
689        );
690    }
691
692    #[tokio::test]
693    async fn test_throttled_writer_no_limit_passthrough() {
694        use crate::filesystem::disk_writer::ByteArrayDiskWriter;
695
696        let raw = ByteArrayDiskWriter::new();
697        let rl = RateLimiter::unlimited();
698        let mut tw = ThrottledWriter::new(raw, rl);
699
700        tw.write(b"hello world").await.unwrap();
701        tw.write(b" foo bar baz").await.unwrap();
702        let result = tw.finalize().await.unwrap();
703
704        assert_eq!(result, b"hello world foo bar baz");
705    }
706
707    #[tokio::test]
708    async fn test_throttled_writer_with_limit() {
709        use crate::filesystem::disk_writer::ByteArrayDiskWriter;
710
711        let raw = ByteArrayDiskWriter::new();
712        let cfg = RateLimiterConfig::new(Some(100_000), None).with_burst(Some(1000), None);
713        let rl = RateLimiter::new(&cfg);
714        let mut tw = ThrottledWriter::new(raw, rl);
715
716        let data = vec![0xABu8; 50_000];
717        let start = Instant::now();
718        tw.write(&data).await.unwrap();
719        let elapsed = start.elapsed();
720
721        let result = tw.finalize().await.unwrap();
722        assert_eq!(result.len(), 50_000);
723        assert!(
724            elapsed >= Duration::from_millis(400),
725            "50KB at 100KB/s with 1KB burst should take >= 400ms, got {:?}",
726            elapsed
727        );
728    }
729
730    #[tokio::test]
731    async fn test_throttled_writer_chunk_size() {
732        use crate::filesystem::disk_writer::ByteArrayDiskWriter;
733
734        let raw = ByteArrayDiskWriter::new();
735        let cfg = RateLimiterConfig::new(Some(1_000_000), None);
736        let rl = RateLimiter::new(&cfg);
737        let mut tw = ThrottledWriter::new(raw, rl).with_chunk_size(1024);
738
739        let large_data = vec![0x42u8; 10_000];
740        tw.write(&large_data).await.unwrap();
741        let result = tw.finalize().await.unwrap();
742        assert_eq!(result.len(), 10_000);
743    }
744
745    #[tokio::test]
746    async fn test_rate_limiter_zero_rate_means_unlimited() {
747        let cfg = RateLimiterConfig::new(Some(0), Some(0));
748        let rl = RateLimiter::new(&cfg);
749        assert!(!rl.is_download_limited());
750        assert!(!rl.is_upload_limited());
751    }
752
753    // ------------------------------------------------------------------
754    // New tests for the lock-free implementation (Task C1 / C2)
755    // ------------------------------------------------------------------
756
757    /// Verify that multiple tasks can acquire from the same `TokenBucket`
758    /// concurrently without deadlock, panic, or excessive contention.
759    ///
760    /// With the old `tokio::sync::Mutex` implementation, 4 concurrent tasks
761    /// would serialise on the mutex. With the lock-free atomic implementation,
762    /// all tasks proceed concurrently — the only blocking is from
763    /// `tokio::time::sleep` when tokens are exhausted.
764    #[tokio::test]
765    async fn test_token_bucket_concurrent_no_deadlock() {
766        // Large burst so all acquires are instant from burst tokens —
767        // this isolates the concurrency test from timing concerns.
768        let bucket = Arc::new(TokenBucket::new(10_000_000, Some(10_000_000)));
769
770        let mut handles = Vec::with_capacity(4);
771        for task_id in 0..4u8 {
772            let b = bucket.clone();
773            handles.push(tokio::spawn(async move {
774                for _ in 0..1000 {
775                    b.acquire(1000).await;
776                }
777                task_id // return id for identification
778            }));
779        }
780
781        // If any task deadlocks or panics, await will fail.
782        for (i, h) in handles.into_iter().enumerate() {
783            let id = h.await.expect("task should complete without panic");
784            assert_eq!(id as usize, i, "task ordering preserved");
785        }
786
787        // After 4 * 1000 * 1000 = 4 MB acquired from a 10 MB burst,
788        // at least 6 MB should remain (minus tiny refill variance).
789        let remaining = bucket.available_tokens();
790        assert!(
791            remaining > 5_000_000.0,
792            "should have ~6MB left after consuming 4MB, got {}",
793            remaining
794        );
795    }
796
797    /// Verify that `ThrottledWriter` batches token acquisition: a single
798    /// `write()` call should result in ONE throttle wait, not per-chunk waits.
799    ///
800    /// We use a rate limiter with zero burst and a moderate rate. The total
801    /// elapsed time should match the batch calculation
802    /// (`total_bytes / rate`), not be inflated by per-chunk sleep scheduling
803    /// overhead. With per-chunk acquisition and a tiny chunk size, the
804    /// many individual `tokio::time::sleep` calls add measurable overhead.
805    #[tokio::test]
806    async fn test_throttled_writer_batches_token_acquisition() {
807        use crate::filesystem::disk_writer::ByteArrayDiskWriter;
808
809        // rate = 10 000 bytes/s, burst = 0 (pure rate limiting, no buffer).
810        // data  = 5 000 bytes → expected wait ~500 ms (one batch sleep).
811        let raw = ByteArrayDiskWriter::new();
812        let cfg = RateLimiterConfig::new(Some(10_000), None).with_burst(Some(0), None);
813        let rl = RateLimiter::new(&cfg);
814        // Tiny chunk size to maximise per-chunk overhead if it were used.
815        let mut tw = ThrottledWriter::new(raw, rl).with_chunk_size(100);
816
817        let data = vec![0x77u8; 5_000];
818        let start = Instant::now();
819        tw.write(&data).await.unwrap();
820        let elapsed = start.elapsed();
821        let result = tw.finalize().await.unwrap();
822
823        assert_eq!(result.len(), 5_000, "data integrity preserved");
824        assert!(
825            result.iter().all(|&b| b == 0x77),
826            "all bytes should be 0x77"
827        );
828
829        // Expected batch wait: 5000 bytes / 10000 bytes/s = 500 ms.
830        // Allow generous lower bound for timer jitter.
831        assert!(
832            elapsed >= Duration::from_millis(450),
833            "batch acquire should wait ~500ms, got {:?}",
834            elapsed
835        );
836
837        // Upper bound: with per-chunk acquisition (50 chunks * 100 bytes),
838        // each 100ms sleep would add scheduling overhead. Batch should be
839        // well under 1 second. If per-chunk were used with 50 sleeps,
840        // overhead would push this higher on most platforms.
841        assert!(
842            elapsed < Duration::from_secs(2),
843            "batch acquire should complete well under 2s, got {:?}",
844            elapsed
845        );
846    }
847
848    /// Verify that `RateLimiter` clones share state — acquiring from one clone
849    /// affects the tokens available to the other. This is a unit-level version
850    /// of the integration test in `test_e2e_rate_limit.rs`.
851    #[tokio::test]
852    async fn test_rate_limiter_clone_shares_state() {
853        let cfg = RateLimiterConfig::new(Some(10000), None).with_burst(Some(5000), None);
854        let rl1 = RateLimiter::new(&cfg);
855        let rl2 = rl1.clone();
856
857        assert!(rl1.is_download_limited());
858        assert!(rl2.is_download_limited());
859
860        // Acquiring from rl1 should deplete tokens visible to rl2.
861        rl1.acquire_download(3000).await;
862        rl2.acquire_download(3000).await;
863
864        let config = rl1.config().await;
865        assert!(config.download_rate().is_some());
866    }
867
868    /// Verify that a high rate with sufficient burst completes near-instantly,
869    /// confirming the lock-free path has negligible overhead.
870    #[tokio::test]
871    async fn test_rate_limiter_high_rate_low_latency() {
872        let cfg = RateLimiterConfig::new(Some(100_000_000), None).with_burst(Some(1_000_000), None);
873        let rl = RateLimiter::new(&cfg);
874
875        let start = Instant::now();
876        rl.acquire_download(100_000).await;
877        let elapsed = start.elapsed();
878        assert!(
879            elapsed < Duration::from_millis(50),
880            "100MB/s rate with 1MB burst should be near-instant for 100KB: got {:?}",
881            elapsed
882        );
883    }
884
885    // ------------------------------------------------------------------
886    // Tests for dynamic rate adjustment
887    // (set_rate / set_unlimited / set_download_rate / set_upload_rate)
888    // ------------------------------------------------------------------
889
890    /// Verify that `set_rate` updates the refill rate dynamically.
891    ///
892    /// Adapted from the task spec: the original version acquired 100 MB at
893    /// 1 MB/s (~100 s wait). We instead drain the small burst with
894    /// `try_acquire` (non-blocking) and then verify the new rate is visible
895    /// via `rate()`.
896    #[tokio::test]
897    async fn test_token_bucket_set_rate() {
898        let tb = TokenBucket::new(1_000_000, Some(1000)); // 1 MB/s, 1 KB burst
899        // Drain the burst tokens (non-blocking).
900        assert!(tb.try_acquire(1000));
901
902        // Now set rate to 10 MB/s and verify.
903        tb.set_rate(10_000_000);
904        let rate = tb.rate();
905        assert!(
906            (rate - 10_000_000.0).abs() < 1.0,
907            "rate should be ~10 MB/s, got {}",
908            rate
909        );
910    }
911
912    /// Verify that `set_rate(0)` reports a zero rate (effectively pauses refill).
913    #[tokio::test]
914    async fn test_token_bucket_set_rate_to_zero() {
915        let tb = TokenBucket::new(1_000_000, Some(1000));
916        tb.set_rate(0);
917        let rate = tb.rate();
918        assert!(
919            (rate - 0.0).abs() < 0.01,
920            "rate should be 0 after set_rate(0), got {}",
921            rate
922        );
923    }
924
925    /// Verify that `set_unlimited(true)` makes acquire return instantly
926    /// even for very large requests.
927    #[tokio::test]
928    async fn test_token_bucket_set_unlimited() {
929        let tb = TokenBucket::new(1_000, None); // 1 KB/s, limited
930        assert!(!tb.is_unlimited());
931        tb.set_unlimited(true);
932        assert!(tb.is_unlimited());
933
934        // Should acquire instantly — 1 GB at 1 KB/s would otherwise take ~17 min.
935        let start = Instant::now();
936        tb.acquire(1_000_000_000).await;
937        let elapsed = start.elapsed();
938        assert!(
939            elapsed < Duration::from_millis(50),
940            "unlimited acquire should be instant, got {:?}",
941            elapsed
942        );
943    }
944
945    /// Verify that `set_download_rate` updates the download rate and that
946    /// `config()` reflects the change.
947    #[tokio::test]
948    async fn test_rate_limiter_set_download_rate() {
949        let rl = RateLimiter::new(&RateLimiterConfig::new(Some(1_000_000), None)); // 1 MB/s
950        assert!(rl.is_download_limited());
951
952        // Change to 5 MB/s
953        rl.set_download_rate(Some(5_000_000));
954        assert!(rl.is_download_limited());
955        let config = rl.config().await;
956        assert_eq!(config.download_rate(), Some(5_000_000));
957
958        // Change to unlimited
959        rl.set_download_rate(None);
960        assert!(!rl.is_download_limited());
961        let config = rl.config().await;
962        assert!(
963            config.download_rate().is_none(),
964            "download_rate should be None after set_download_rate(None), got {:?}",
965            config.download_rate()
966        );
967    }
968
969    /// Verify that `set_upload_rate` updates the upload rate and that
970    /// `config()` reflects the change.
971    #[tokio::test]
972    async fn test_rate_limiter_set_upload_rate() {
973        let rl = RateLimiter::new(&RateLimiterConfig::new(None, Some(500_000))); // 500 KB/s
974        assert!(rl.is_upload_limited());
975
976        rl.set_upload_rate(Some(2_000_000)); // 2 MB/s
977        assert!(rl.is_upload_limited());
978        let config = rl.config().await;
979        assert_eq!(config.upload_rate(), Some(2_000_000));
980
981        // Change to unlimited via Some(0)
982        rl.set_upload_rate(Some(0));
983        assert!(!rl.is_upload_limited());
984        let config = rl.config().await;
985        assert!(config.upload_rate().is_none());
986    }
987
988    /// Verify that `RateLimiter` clones share the inner token bucket state —
989    /// changing the rate via one clone is visible through `config()` and
990    /// `is_download_limited()` on another. Both the rate and the limited flag
991    /// live inside `Arc<RateLimiterInner>`, so all clones observe updates.
992    #[tokio::test]
993    async fn test_rate_limiter_clone_shares_inner_state() {
994        let rl = RateLimiter::new(&RateLimiterConfig::new(Some(1_000_000), None));
995        let rl_clone = rl.clone();
996
997        // Change rate via original
998        rl.set_download_rate(Some(5_000_000));
999
1000        // Clone should see the updated rate via the shared inner.
1001        let config = rl_clone.config().await;
1002        assert_eq!(
1003            config.download_rate(),
1004            Some(5_000_000),
1005            "clone should see updated rate via shared Arc<inner>"
1006        );
1007        assert!(
1008            rl_clone.is_download_limited(),
1009            "clone should see updated limited flag via shared Arc<inner>"
1010        );
1011
1012        // Change to unlimited via the clone — original should see it too.
1013        rl_clone.set_download_rate(None);
1014        assert!(
1015            !rl.is_download_limited(),
1016            "original should see unlimited flag set by clone"
1017        );
1018    }
1019}