shardline_server/
transfer_limiter.rs1use std::{num::NonZeroUsize, sync::Arc};
2
3use tokio::sync::{OwnedSemaphorePermit, Semaphore};
4
5use crate::ServerError;
6
7#[derive(Debug, Clone)]
9pub struct TransferLimiter {
10 chunk_size_bytes: NonZeroUsize,
11 max_in_flight_chunks: NonZeroUsize,
12 semaphore: Arc<Semaphore>,
13}
14
15impl TransferLimiter {
16 #[must_use]
19 pub fn new(chunk_size_bytes: NonZeroUsize, max_in_flight_chunks: NonZeroUsize) -> Self {
20 Self {
21 chunk_size_bytes,
22 max_in_flight_chunks,
23 semaphore: Arc::new(Semaphore::new(max_in_flight_chunks.get())),
24 }
25 }
26
27 pub async fn acquire_bytes(
34 &self,
35 total_bytes: u64,
36 ) -> Result<OwnedSemaphorePermit, ServerError> {
37 let permits = permits_for_bytes(
38 total_bytes,
39 self.chunk_size_bytes,
40 self.max_in_flight_chunks,
41 )?;
42 self.semaphore
43 .clone()
44 .acquire_many_owned(permits)
45 .await
46 .map_err(|_error| ServerError::TransferLimiterClosed)
47 }
48}
49
50fn permits_for_bytes(
51 total_bytes: u64,
52 chunk_size_bytes: NonZeroUsize,
53 max_in_flight_chunks: NonZeroUsize,
54) -> Result<u32, ServerError> {
55 let effective_bytes = total_bytes.max(1);
56 let chunk_size = u64::try_from(chunk_size_bytes.get())?;
57 let required_chunks = effective_bytes.div_ceil(chunk_size);
58 let max_chunks = u64::try_from(max_in_flight_chunks.get())?;
59 let bounded_chunks = required_chunks.min(max_chunks);
60 u32::try_from(bounded_chunks).map_err(ServerError::from)
61}
62
63#[cfg(test)]
64mod tests {
65 use std::{num::NonZeroUsize, time::Duration};
66
67 use tokio::time::timeout;
68
69 use super::{TransferLimiter, permits_for_bytes};
70
71 const CHUNK_SIZE: NonZeroUsize = match NonZeroUsize::new(4) {
72 Some(value) => value,
73 None => NonZeroUsize::MIN,
74 };
75
76 const MAX_IN_FLIGHT: NonZeroUsize = match NonZeroUsize::new(3) {
77 Some(value) => value,
78 None => NonZeroUsize::MIN,
79 };
80
81 #[test]
82 fn permits_scale_with_bytes_and_cap_at_capacity() {
83 let one_byte = permits_for_bytes(1, CHUNK_SIZE, MAX_IN_FLIGHT);
84 let exact_chunk = permits_for_bytes(4, CHUNK_SIZE, MAX_IN_FLIGHT);
85 let two_chunks = permits_for_bytes(5, CHUNK_SIZE, MAX_IN_FLIGHT);
86 let capped = permits_for_bytes(20, CHUNK_SIZE, MAX_IN_FLIGHT);
87
88 assert!(one_byte.is_ok());
89 assert!(exact_chunk.is_ok());
90 assert!(two_chunks.is_ok());
91 assert!(capped.is_ok());
92 assert_eq!(one_byte.ok(), Some(1));
93 assert_eq!(exact_chunk.ok(), Some(1));
94 assert_eq!(two_chunks.ok(), Some(2));
95 assert_eq!(capped.ok(), Some(3));
96 }
97
98 #[test]
99 fn zero_byte_transfers_still_reserve_one_permit() {
100 let permits = permits_for_bytes(0, CHUNK_SIZE, MAX_IN_FLIGHT);
101
102 assert!(permits.is_ok());
103 assert_eq!(permits.ok(), Some(1));
104 }
105
106 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
107 async fn limiter_blocks_until_capacity_returns() {
108 let limiter = TransferLimiter::new(CHUNK_SIZE, NonZeroUsize::MIN);
109 let first = limiter.acquire_bytes(4).await;
110 assert!(first.is_ok());
111 let Ok(first) = first else {
112 return;
113 };
114
115 let blocked = timeout(Duration::from_millis(50), limiter.acquire_bytes(4)).await;
116 assert!(blocked.is_err());
117
118 drop(first);
119
120 let released = timeout(Duration::from_secs(1), limiter.acquire_bytes(4)).await;
121 assert!(released.is_ok());
122 let Ok(released) = released else {
123 return;
124 };
125 assert!(released.is_ok());
126 }
127}