Skip to main content

openrtc_file_transfer/
flow_control.rs

1//! Shared pacing of application body bytes. One controller serves all bulk
2//! executors in a runtime; admission, ACKs and lifecycle control bypass it.
3//! Call `acquire` before starting transport I/O deadlines. Dropping the future
4//! cancels a queued slice without leaving a peer slot or timer behind.
5use std::collections::{HashMap, HashSet, VecDeque};
6use std::sync::{Arc, Mutex};
7use tokio::sync::{oneshot, Notify};
8use tokio::time::{Duration, Instant};
9
10pub const MAX_BULK_BURST_BYTES: u64 = 64 * 1024;
11const MAX_PENDING: usize = 4096;
12const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum BulkDirection {
16    Upload,
17    Download,
18}
19impl BulkDirection {
20    fn index(self) -> usize {
21        match self {
22            Self::Upload => 0,
23            Self::Download => 1,
24        }
25    }
26}
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct BulkRates {
29    pub upload_bytes_per_second: Option<u64>,
30    pub download_bytes_per_second: Option<u64>,
31}
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum BulkFlowError {
34    InvalidRate,
35    InvalidSlice,
36    QueueFull,
37    OwnerClosed,
38}
39impl std::fmt::Display for BulkFlowError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str(match self {
42            Self::InvalidRate => "Bulk rate must be unset or a positive safe integer",
43            Self::InvalidSlice => "Invalid bulk body slice",
44            Self::QueueFull => "receiver_busy: bulk flow queue is full",
45            Self::OwnerClosed => "execution_suspended: bulk flow owner closed",
46        })
47    }
48}
49impl std::error::Error for BulkFlowError {}
50struct Pending {
51    id: u64,
52    bytes: u64,
53    scope: Option<String>,
54    ready: oneshot::Sender<()>,
55}
56struct ScopedBudget {
57    rate: u64,
58    tokens: f64,
59    updated: Instant,
60}
61impl ScopedBudget {
62    fn refill(&mut self) {
63        let now = Instant::now();
64        self.tokens = (self.tokens
65            + now.saturating_duration_since(self.updated).as_secs_f64() * self.rate as f64)
66            .min(MAX_BULK_BURST_BYTES as f64);
67        self.updated = now;
68    }
69}
70struct DirectionState {
71    rate: Option<u64>,
72    tokens: f64,
73    updated: Instant,
74    peers: HashMap<String, VecDeque<Pending>>,
75    order: VecDeque<String>,
76    scopes: HashMap<String, ScopedBudget>,
77}
78impl DirectionState {
79    fn new() -> Self {
80        Self {
81            rate: None,
82            tokens: MAX_BULK_BURST_BYTES as f64,
83            updated: Instant::now(),
84            peers: HashMap::new(),
85            order: VecDeque::new(),
86            scopes: HashMap::new(),
87        }
88    }
89    fn refill(&mut self) {
90        let now = Instant::now();
91        if let Some(rate) = self.rate {
92            self.tokens = (self.tokens
93                + now.saturating_duration_since(self.updated).as_secs_f64() * rate as f64)
94                .min(MAX_BULK_BURST_BYTES as f64);
95        }
96        self.updated = now;
97    }
98    fn admit_scope(&mut self, key: &str, rate: u64) -> Result<(), BulkFlowError> {
99        if let Some(scope) = self.scopes.get(key) {
100            return if scope.rate == rate {
101                Ok(())
102            } else {
103                Err(BulkFlowError::InvalidRate)
104            };
105        }
106        // A queued slice owns its scope. Other scopes may retire only after
107        // their full burst could refill, preserving debt across short retries.
108        let active: HashSet<_> = self
109            .peers
110            .values()
111            .flat_map(|queue| queue.iter())
112            .filter_map(|slice| slice.scope.clone())
113            .collect();
114        self.scopes.retain(|key, scope| {
115            active.contains(key)
116                || scope.updated.elapsed().as_secs_f64() * (scope.rate as f64)
117                    < MAX_BULK_BURST_BYTES as f64
118        });
119        if self.scopes.len() >= MAX_PENDING {
120            return Err(BulkFlowError::QueueFull);
121        }
122        self.scopes.insert(
123            key.into(),
124            ScopedBudget {
125                rate,
126                tokens: MAX_BULK_BURST_BYTES as f64,
127                updated: Instant::now(),
128            },
129        );
130        Ok(())
131    }
132    fn pump(&mut self) -> Option<Duration> {
133        self.refill();
134        let mut blocked = 0;
135        let mut wait: Option<Duration> = None;
136        while !self.order.is_empty() && blocked < self.order.len() {
137            let peer = self.order.pop_front().expect("flow peer order");
138            let queue = self.peers.get_mut(&peer).expect("flow peer order");
139            while queue.front().is_some_and(|slice| slice.ready.is_closed()) {
140                queue.pop_front();
141            }
142            if queue.is_empty() {
143                self.peers.remove(&peer);
144                continue;
145            }
146            let front = queue.front().expect("pending flow slice");
147            let bytes = front.bytes;
148            let scope_key = front.scope.clone();
149            let global_delay = self
150                .rate
151                .filter(|_| self.tokens < bytes as f64)
152                .map(|rate| (bytes as f64 - self.tokens) / rate as f64);
153            let mut scoped_delay = None;
154            if let Some(key) = &scope_key {
155                let scope = self.scopes.get_mut(key).expect("queued flow scope");
156                scope.refill();
157                if scope.tokens < bytes as f64 {
158                    let scoped = (bytes as f64 - scope.tokens) / scope.rate as f64;
159                    scoped_delay = Some(scoped);
160                }
161            }
162            // An aggregate-ready peer keeps its turn while global tokens
163            // refill. Skipping it would let smaller slices starve a large one.
164            if scoped_delay.is_none() {
165                if let Some(delay) = global_delay {
166                    let delay = Duration::from_secs_f64(delay.max(0.000_000_001));
167                    self.order.push_front(peer);
168                    return Some(wait.map_or(delay, |prior| prior.min(delay)));
169                }
170            }
171            let delay = match (global_delay, scoped_delay) {
172                (Some(global), Some(scoped)) => Some(global.max(scoped)),
173                (global, scoped) => global.or(scoped),
174            };
175            if let Some(delay) = delay {
176                // Do not reserve aggregate tokens for a scope that cannot run.
177                // Other ready peers retain their fair opportunity this turn.
178                let delay = Duration::from_secs_f64(delay.max(0.000_000_001));
179                wait = Some(wait.map_or(delay, |prior| prior.min(delay)));
180                self.order.push_back(peer);
181                blocked += 1;
182                continue;
183            }
184            if self.rate.is_some() {
185                self.tokens -= bytes as f64;
186            }
187            if let Some(key) = scope_key {
188                self.scopes.get_mut(&key).expect("queued flow scope").tokens -= bytes as f64;
189            }
190            let slice = queue.pop_front().expect("pending flow slice");
191            if queue.is_empty() {
192                self.peers.remove(&peer);
193            } else {
194                self.order.push_back(peer);
195            }
196            let _ = slice.ready.send(());
197            blocked = 0;
198        }
199        wait
200    }
201}
202struct FlowState {
203    directions: [DirectionState; 2],
204    next_id: u64,
205}
206struct FlowInner {
207    state: Mutex<FlowState>,
208    changed: Notify,
209}
210/// Clone this handle, rather than constructing a controller per transfer.
211#[derive(Clone)]
212pub struct BulkFlowController {
213    inner: Arc<FlowInner>,
214}
215struct WaitingSlice {
216    owner: Arc<FlowInner>,
217    direction: usize,
218    peer: String,
219    id: u64,
220}
221impl Drop for WaitingSlice {
222    fn drop(&mut self) {
223        let mut state = self
224            .owner
225            .state
226            .lock()
227            .unwrap_or_else(std::sync::PoisonError::into_inner);
228        let direction = &mut state.directions[self.direction];
229        if let Some(queue) = direction.peers.get_mut(&self.peer) {
230            queue.retain(|slice| slice.id != self.id);
231            if queue.is_empty() {
232                direction.peers.remove(&self.peer);
233                direction.order.retain(|peer| peer != &self.peer);
234            }
235        }
236        direction.pump();
237        drop(state);
238        self.owner.changed.notify_waiters();
239    }
240}
241impl BulkFlowController {
242    pub fn new(rates: BulkRates) -> Result<Self, BulkFlowError> {
243        let result = Self {
244            inner: Arc::new(FlowInner {
245                state: Mutex::new(FlowState {
246                    directions: [DirectionState::new(), DirectionState::new()],
247                    next_id: 0,
248                }),
249                changed: Notify::new(),
250            }),
251        };
252        result.configure(rates)?;
253        Ok(result)
254    }
255    pub fn configure(&self, rates: BulkRates) -> Result<(), BulkFlowError> {
256        let rates = [
257            rates.upload_bytes_per_second,
258            rates.download_bytes_per_second,
259        ];
260        if rates
261            .into_iter()
262            .flatten()
263            .any(|rate| rate == 0 || rate > MAX_SAFE_INTEGER)
264        {
265            return Err(BulkFlowError::InvalidRate);
266        }
267        let mut state = self
268            .inner
269            .state
270            .lock()
271            .unwrap_or_else(std::sync::PoisonError::into_inner);
272        for (direction, rate) in state.directions.iter_mut().zip(rates) {
273            direction.refill();
274            direction.rate = rate;
275            direction.pump();
276        }
277        drop(state);
278        self.inner.changed.notify_waiters();
279        Ok(())
280    }
281    pub async fn acquire(
282        &self,
283        direction: BulkDirection,
284        peer: &str,
285        bytes: u64,
286    ) -> Result<(), BulkFlowError> {
287        self.acquire_inner(direction, peer, None, bytes).await
288    }
289    /// Admit a body slice against both aggregate and immutable scoped rates
290    /// in one decision. Reuse a stable scope across attempts; controls bypass it.
291    pub async fn acquire_scoped(
292        &self,
293        direction: BulkDirection,
294        peer: &str,
295        scope: &str,
296        bytes_per_second: Option<u64>,
297        bytes: u64,
298    ) -> Result<(), BulkFlowError> {
299        if scope.is_empty()
300            || scope.len() > 128
301            || scope.chars().any(char::is_control)
302            || bytes_per_second.is_some_and(|rate| rate == 0 || rate > MAX_SAFE_INTEGER)
303        {
304            return Err(BulkFlowError::InvalidRate);
305        }
306        self.acquire_inner(
307            direction,
308            peer,
309            bytes_per_second.map(|rate| (scope, rate)),
310            bytes,
311        )
312        .await
313    }
314    async fn acquire_inner(
315        &self,
316        direction: BulkDirection,
317        peer: &str,
318        scope: Option<(&str, u64)>,
319        bytes: u64,
320    ) -> Result<(), BulkFlowError> {
321        if peer.is_empty()
322            || peer.len() > 512
323            || peer.chars().any(char::is_control)
324            || bytes > MAX_BULK_BURST_BYTES
325        {
326            return Err(BulkFlowError::InvalidSlice);
327        }
328        if bytes == 0 {
329            return Ok(());
330        }
331        let direction = direction.index();
332        let (send, mut receive) = oneshot::channel();
333        let id = {
334            let mut state = self
335                .inner
336                .state
337                .lock()
338                .unwrap_or_else(std::sync::PoisonError::into_inner);
339            let pending: usize = state
340                .directions
341                .iter()
342                .flat_map(|state| state.peers.values())
343                .map(VecDeque::len)
344                .sum();
345            if pending >= MAX_PENDING {
346                return Err(BulkFlowError::QueueFull);
347            }
348            if let Some((key, rate)) = scope {
349                state.directions[direction].admit_scope(key, rate)?;
350            }
351            let id = state
352                .next_id
353                .checked_add(1)
354                .ok_or(BulkFlowError::QueueFull)?;
355            state.next_id = id;
356            let state = &mut state.directions[direction];
357            if !state.peers.contains_key(peer) {
358                state.order.push_back(peer.into());
359            }
360            state
361                .peers
362                .entry(peer.into())
363                .or_default()
364                .push_back(Pending {
365                    id,
366                    bytes,
367                    scope: scope.map(|(key, _)| key.into()),
368                    ready: send,
369                });
370            state.pump();
371            id
372        };
373        let _lease = WaitingSlice {
374            owner: self.inner.clone(),
375            direction,
376            peer: peer.into(),
377            id,
378        };
379        loop {
380            // Register before inspecting state so policy updates/cancellation
381            // cannot be missed between choosing a deadline and sleeping.
382            let changed = self.inner.changed.notified();
383            tokio::pin!(changed);
384            changed.as_mut().enable();
385            let wait = {
386                let mut state = self
387                    .inner
388                    .state
389                    .lock()
390                    .unwrap_or_else(std::sync::PoisonError::into_inner);
391                state.directions[direction].pump()
392            };
393            tokio::select! {
394                result = &mut receive => return result.map_err(|_| BulkFlowError::OwnerClosed),
395                _ = &mut changed => {},
396                _ = async { match wait { Some(delay) => tokio::time::sleep(delay).await,
397                    None => std::future::pending().await } } => {},
398            }
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[tokio::test(start_paused = true)]
408    async fn aggregate_ready_large_slice_keeps_its_turn_ahead_of_later_small_slices() {
409        let flow = BulkFlowController::new(BulkRates {
410            upload_bytes_per_second: Some(1024),
411            ..BulkRates::default()
412        })
413        .unwrap();
414        flow.acquire(BulkDirection::Upload, "initial", MAX_BULK_BURST_BYTES)
415            .await
416            .unwrap();
417        let large = {
418            let flow = flow.clone();
419            tokio::spawn(async move {
420                flow.acquire(BulkDirection::Upload, "large", MAX_BULK_BURST_BYTES)
421                    .await
422            })
423        };
424        tokio::task::yield_now().await;
425        let small = {
426            let flow = flow.clone();
427            tokio::spawn(async move { flow.acquire(BulkDirection::Upload, "small", 1024).await })
428        };
429        tokio::task::yield_now().await;
430        tokio::time::advance(Duration::from_secs(1)).await;
431        tokio::task::yield_now().await;
432        tokio::task::yield_now().await;
433        assert!(
434            !small.is_finished(),
435            "Later small slices consumed credit owed to an aggregate-ready large peer"
436        );
437        assert!(!large.is_finished());
438        tokio::time::advance(Duration::from_secs(63)).await;
439        tokio::task::yield_now().await;
440        tokio::task::yield_now().await;
441        assert!(large.is_finished());
442        large.await.unwrap().unwrap();
443        assert!(!small.is_finished());
444        tokio::time::advance(Duration::from_secs(1)).await;
445        small.await.unwrap().unwrap();
446    }
447
448    // Native timers and dropped futures have different ownership from the JS
449    // controller. Assert public grants against independently measured time.
450    #[tokio::test(start_paused = true)]
451    async fn parallel_clones_share_one_burst_and_rotate_peers_before_second_slices() {
452        let flow = BulkFlowController::new(BulkRates {
453            upload_bytes_per_second: Some(1024),
454            download_bytes_per_second: Some(2048),
455        })
456        .unwrap();
457        let start = Instant::now();
458        flow.acquire(BulkDirection::Upload, "a", MAX_BULK_BURST_BYTES)
459            .await
460            .unwrap();
461        let observed = Arc::new(Mutex::new(Vec::new()));
462        let mut tasks = Vec::new();
463        for (name, peer) in [("a1", "a"), ("a2", "a"), ("b1", "b")] {
464            let (flow, observed) = (flow.clone(), observed.clone());
465            tasks.push(tokio::spawn(async move {
466                flow.acquire(BulkDirection::Upload, peer, MAX_BULK_BURST_BYTES)
467                    .await
468                    .unwrap();
469                observed
470                    .lock()
471                    .unwrap()
472                    .push((name, Instant::now().duration_since(start)));
473            }));
474            tokio::task::yield_now().await;
475        }
476        assert!(
477            observed.lock().unwrap().is_empty(),
478            "clones must not each get a fresh burst"
479        );
480        // Upload consumption cannot consume the independent download burst.
481        flow.acquire(BulkDirection::Download, "b", MAX_BULK_BURST_BYTES)
482            .await
483            .unwrap();
484        for _ in 0..3 {
485            tokio::time::advance(Duration::from_secs(64)).await;
486            tokio::task::yield_now().await;
487            tokio::task::yield_now().await;
488        }
489        for task in tasks {
490            task.await.unwrap();
491        }
492        let events = observed.lock().unwrap();
493        assert_eq!(
494            events.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
495            ["a1", "b1", "a2"]
496        );
497        for (index, (_, elapsed)) in events.iter().enumerate() {
498            let attempted = (index as u64 + 2) * MAX_BULK_BURST_BYTES;
499            assert!(attempted <= MAX_BULK_BURST_BYTES + elapsed.as_secs() * 1024);
500        }
501    }
502
503    #[tokio::test(start_paused = true)]
504    async fn cancelled_head_releases_waiters_and_live_limits_wake_only_their_direction() {
505        let flow = BulkFlowController::new(BulkRates {
506            upload_bytes_per_second: Some(1024),
507            download_bytes_per_second: Some(1024),
508        })
509        .unwrap();
510        for direction in [BulkDirection::Upload, BulkDirection::Download] {
511            flow.acquire(direction, "initial", MAX_BULK_BURST_BYTES)
512                .await
513                .unwrap();
514        }
515        let head = {
516            let flow = flow.clone();
517            tokio::spawn(async move {
518                flow.acquire(BulkDirection::Upload, "cancelled", MAX_BULK_BURST_BYTES)
519                    .await
520            })
521        };
522        tokio::task::yield_now().await;
523        let smaller = {
524            let flow = flow.clone();
525            tokio::spawn(async move { flow.acquire(BulkDirection::Upload, "other", 1024).await })
526        };
527        tokio::task::yield_now().await;
528        head.abort();
529        let _ = head.await;
530        tokio::time::advance(Duration::from_secs(1)).await;
531        tokio::task::yield_now().await;
532        assert!(
533            smaller.is_finished(),
534            "cancelled head cannot leave a 64-second deadline behind"
535        );
536        smaller.await.unwrap().unwrap();
537        let upload = {
538            let flow = flow.clone();
539            tokio::spawn(async move {
540                flow.acquire(BulkDirection::Upload, "a", MAX_BULK_BURST_BYTES)
541                    .await
542            })
543        };
544        let download = {
545            let flow = flow.clone();
546            tokio::spawn(async move {
547                flow.acquire(BulkDirection::Download, "a", MAX_BULK_BURST_BYTES)
548                    .await
549            })
550        };
551        tokio::task::yield_now().await;
552        assert!(!upload.is_finished());
553        assert!(!download.is_finished());
554        flow.configure(BulkRates {
555            upload_bytes_per_second: None,
556            download_bytes_per_second: Some(1024),
557        })
558        .unwrap();
559        tokio::task::yield_now().await;
560        assert!(upload.is_finished());
561        assert!(!download.is_finished());
562        upload.await.unwrap().unwrap();
563        download.abort();
564        let _ = download.await;
565        assert_eq!(
566            flow.acquire(BulkDirection::Upload, "a", MAX_BULK_BURST_BYTES + 1)
567                .await,
568            Err(BulkFlowError::InvalidSlice)
569        );
570        assert_eq!(
571            flow.configure(BulkRates {
572                upload_bytes_per_second: Some(0),
573                download_bytes_per_second: None
574            }),
575            Err(BulkFlowError::InvalidRate)
576        );
577    }
578}