nzb_dispatch/
bandwidth.rs1use std::num::NonZeroU32;
2use std::sync::atomic::{AtomicU32, Ordering};
3
4use arc_swap::ArcSwapOption;
5use governor::DefaultDirectRateLimiter as RateLimiter;
6use governor::Quota;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use tokio::sync::Notify;
10
11#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
12pub struct BandwidthConfig {
13 pub download_bps: Option<NonZeroU32>,
15}
16
17struct Bucket {
20 limiter: RateLimiter,
21 burst: NonZeroU32,
22}
23
24struct Limit {
25 limiter: ArcSwapOption<Bucket>,
26 current_bps: AtomicU32,
27 changed: Notify,
30}
31
32impl Limit {
33 fn new_inner(bps: Option<NonZeroU32>) -> Option<Arc<Bucket>> {
34 let bps = bps?;
35 Some(Arc::new(Bucket {
36 limiter: RateLimiter::direct(Quota::per_second(bps)),
37 burst: bps,
38 }))
39 }
40
41 fn new(bps: Option<NonZeroU32>) -> Self {
42 Self {
43 limiter: ArcSwapOption::new(Self::new_inner(bps)),
44 current_bps: AtomicU32::new(bps.map(|v| v.get()).unwrap_or(0)),
45 changed: Notify::new(),
46 }
47 }
48
49 async fn acquire(&self, size: NonZeroU32) -> anyhow::Result<()> {
50 let mut remaining = size.get();
51 while remaining > 0 {
52 let changed = self.changed.notified();
55 tokio::pin!(changed);
56 changed.as_mut().enable();
57
58 let lim = self.limiter.load_full();
59 let Some(bucket) = lim else {
60 return Ok(());
62 };
63 let chunk = remaining.min(bucket.burst.get());
69 let n = NonZeroU32::new(chunk).expect("chunk > 0");
71 tokio::select! {
72 res = bucket.limiter.until_n_ready(n) => {
73 res?;
74 remaining -= chunk;
75 }
76 () = &mut changed => {}
79 }
80 }
81 Ok(())
82 }
83
84 fn set(&self, limit: Option<NonZeroU32>) {
85 let new = Self::new_inner(limit);
86 self.limiter.swap(new);
87 self.current_bps
88 .store(limit.map(|v| v.get()).unwrap_or(0), Ordering::Relaxed);
89 self.changed.notify_waiters();
90 }
91
92 fn get(&self) -> Option<NonZeroU32> {
93 NonZeroU32::new(self.current_bps.load(Ordering::Relaxed))
94 }
95}
96
97pub struct BandwidthLimiter {
98 download: Limit,
99}
100
101impl BandwidthLimiter {
102 pub fn new(config: BandwidthConfig) -> Self {
103 Self {
104 download: Limit::new(config.download_bps),
105 }
106 }
107
108 pub async fn acquire_download(&self, len: NonZeroU32) -> anyhow::Result<()> {
109 self.download.acquire(len).await
110 }
111
112 pub fn set_download_bps(&self, bps: Option<NonZeroU32>) {
113 self.download.set(bps);
114 }
115
116 pub fn get_download_bps(&self) -> Option<NonZeroU32> {
117 self.download.get()
118 }
119
120 pub fn get_config(&self) -> BandwidthConfig {
121 BandwidthConfig {
122 download_bps: self.get_download_bps(),
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[tokio::test]
136 async fn request_larger_than_burst_is_throttled_not_rejected() {
137 let limiter = BandwidthLimiter::new(BandwidthConfig {
138 download_bps: NonZeroU32::new(200_000),
139 });
140 let start = std::time::Instant::now();
141 limiter
142 .acquire_download(NonZeroU32::new(500_000).unwrap())
143 .await
144 .expect("oversized request must be throttled, not rejected");
145 assert!(
147 start.elapsed() >= std::time::Duration::from_millis(1_000),
148 "500 KB at 200 KB/s returned after {:?}",
149 start.elapsed()
150 );
151 }
152
153 async fn acquire_then_reconfigure(
156 initial: u32,
157 changed: Option<NonZeroU32>,
158 ) -> std::time::Duration {
159 let limiter = Arc::new(BandwidthLimiter::new(BandwidthConfig {
160 download_bps: NonZeroU32::new(initial),
161 }));
162 let waiter = {
163 let limiter = Arc::clone(&limiter);
164 tokio::spawn(async move {
165 limiter
167 .acquire_download(NonZeroU32::new(750_000).unwrap())
168 .await
169 })
170 };
171 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
173 assert!(!waiter.is_finished(), "acquire should be throttled");
174
175 let changed_at = std::time::Instant::now();
176 limiter.set_download_bps(changed);
177 tokio::time::timeout(std::time::Duration::from_secs(5), waiter)
178 .await
179 .expect("parked acquire never observed the limit change")
180 .unwrap()
181 .unwrap();
182 changed_at.elapsed()
183 }
184
185 #[tokio::test]
189 async fn switching_to_unlimited_releases_parked_acquire() {
190 let took = acquire_then_reconfigure(20, None).await;
191 assert!(
192 took < std::time::Duration::from_secs(2),
193 "unlimited took {took:?} to apply"
194 );
195 }
196
197 #[tokio::test]
198 async fn raising_limit_releases_parked_acquire() {
199 let took = acquire_then_reconfigure(20, NonZeroU32::new(10_000_000)).await;
200 assert!(
201 took < std::time::Duration::from_secs(2),
202 "raised limit took {took:?} to apply"
203 );
204 }
205
206 #[tokio::test]
207 async fn unlimited_acquire_returns_immediately() {
208 let limiter = BandwidthLimiter::new(BandwidthConfig::default());
209 tokio::time::timeout(
210 std::time::Duration::from_millis(100),
211 limiter.acquire_download(NonZeroU32::new(u32::MAX).unwrap()),
212 )
213 .await
214 .expect("unlimited acquire must not wait")
215 .unwrap();
216 }
217
218 #[tokio::test]
219 async fn limiter_can_be_reconfigured_without_recreation() {
220 let limiter = BandwidthLimiter::new(BandwidthConfig::default());
221 assert_eq!(limiter.get_download_bps(), None);
222 limiter
223 .acquire_download(NonZeroU32::new(1).unwrap())
224 .await
225 .unwrap();
226
227 limiter.set_download_bps(NonZeroU32::new(1_000));
228 assert_eq!(limiter.get_config().download_bps.unwrap().get(), 1_000);
229 limiter.set_download_bps(None);
230 assert_eq!(limiter.get_download_bps(), None);
231 }
232}