1use crate::error::FaucetError;
25use futures::{StreamExt, TryStreamExt};
26use std::future::Future;
27
28pub fn chunks<T>(items: &[T], batch_size: usize) -> impl ExactSizeIterator<Item = &[T]> {
32 let size = if batch_size == 0 {
33 items.len().max(1)
34 } else {
35 batch_size
36 };
37 items.chunks(size)
38}
39
40pub fn into_chunks<T>(items: Vec<T>, batch_size: usize) -> Vec<Vec<T>> {
42 if items.is_empty() {
43 return Vec::new();
44 }
45 if batch_size == 0 || items.len() <= batch_size {
46 return vec![items];
47 }
48 let mut out = Vec::with_capacity(items.len().div_ceil(batch_size));
49 let mut iter = items.into_iter();
50 loop {
51 let chunk: Vec<T> = iter.by_ref().take(batch_size).collect();
52 if chunk.is_empty() {
53 return out;
54 }
55 out.push(chunk);
56 }
57}
58
59pub async fn for_each_concurrent<I, F, Fut>(items: I, limit: usize, f: F) -> Result<(), FaucetError>
63where
64 I: IntoIterator,
65 F: FnMut(I::Item) -> Fut,
66 Fut: Future<Output = Result<(), FaucetError>>,
67{
68 futures::stream::iter(items)
69 .map(f)
70 .buffer_unordered(limit.max(1))
71 .try_for_each(|()| async { Ok(()) })
72 .await
73}
74
75pub async fn map_concurrent<I, F, Fut, T>(
79 items: I,
80 limit: usize,
81 f: F,
82) -> Result<Vec<T>, FaucetError>
83where
84 I: IntoIterator,
85 F: FnMut(I::Item) -> Fut,
86 Fut: Future<Output = Result<T, FaucetError>>,
87{
88 futures::stream::iter(items)
89 .map(f)
90 .buffered(limit.max(1))
91 .try_collect()
92 .await
93}
94
95#[derive(Debug, Clone)]
101pub struct ConcurrencyLimit {
102 permits: std::sync::Arc<tokio::sync::Semaphore>,
103 limit: usize,
104}
105
106impl ConcurrencyLimit {
107 pub fn new(n: usize) -> Self {
109 let limit = n.max(1);
110 Self {
111 permits: std::sync::Arc::new(tokio::sync::Semaphore::new(limit)),
112 limit,
113 }
114 }
115
116 pub fn limit(&self) -> usize {
118 self.limit
119 }
120
121 pub async fn run<T>(&self, fut: impl Future<Output = T>) -> T {
123 let _permit = self
124 .permits
125 .acquire()
126 .await
127 .expect("the semaphore is never closed");
128 fut.await
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::sync::Arc;
136 use std::sync::atomic::{AtomicUsize, Ordering};
137 use std::time::Duration;
138
139 #[test]
140 fn chunks_honour_the_batch_size_contract() {
141 let v: Vec<u8> = (0..5).collect();
142 let sizes: Vec<usize> = chunks(&v, 2).map(<[u8]>::len).collect();
143 assert_eq!(sizes, vec![2, 2, 1]);
144 assert_eq!(chunks(&v, 0).len(), 1, "0 = one chunk");
145 assert_eq!(chunks(&v, 0).next().unwrap().len(), 5);
146 assert_eq!(chunks(&v, 9).len(), 1);
147 assert_eq!(chunks::<u8>(&[], 0).len(), 0);
148 assert_eq!(chunks::<u8>(&[], 3).len(), 0);
149 }
150
151 #[test]
152 fn into_chunks_moves_items() {
153 assert_eq!(
154 into_chunks(vec![1, 2, 3, 4, 5], 2),
155 vec![vec![1, 2], vec![3, 4], vec![5]]
156 );
157 assert_eq!(into_chunks(vec![1, 2, 3], 0), vec![vec![1, 2, 3]]);
158 assert_eq!(into_chunks(vec![1, 2], 2), vec![vec![1, 2]]);
159 assert!(into_chunks(Vec::<u8>::new(), 2).is_empty());
160 assert_eq!(into_chunks(vec![1, 2, 3, 4], 2).len(), 2);
161 }
162
163 #[tokio::test(start_paused = true)]
164 async fn concurrency_is_bounded_and_order_kept() {
165 let live = Arc::new(AtomicUsize::new(0));
166 let peak = Arc::new(AtomicUsize::new(0));
167 let out = map_concurrent(0..8u64, 3, |i| {
168 let (live, peak) = (live.clone(), peak.clone());
169 async move {
170 let now = live.fetch_add(1, Ordering::SeqCst) + 1;
171 peak.fetch_max(now, Ordering::SeqCst);
172 #[allow(clippy::disallowed_methods)]
173 tokio::time::sleep(Duration::from_millis(10 * (8 - i))).await;
174 live.fetch_sub(1, Ordering::SeqCst);
175 Ok(i * 2)
176 }
177 })
178 .await
179 .unwrap();
180 assert_eq!(out, (0..8).map(|i| i * 2).collect::<Vec<_>>());
181 assert_eq!(peak.load(Ordering::SeqCst), 3);
182 let done = Arc::new(AtomicUsize::new(0));
183 let d = done.clone();
184 for_each_concurrent(0..5, 0, move |_| {
185 let d = d.clone();
186 async move {
187 d.fetch_add(1, Ordering::SeqCst);
188 Ok(())
189 }
190 })
191 .await
192 .unwrap();
193 assert_eq!(done.load(Ordering::SeqCst), 5);
194 }
195
196 #[tokio::test(start_paused = true)]
197 async fn a_shared_limit_bounds_separate_calls() {
198 let limit = ConcurrencyLimit::new(0);
199 assert_eq!(limit.limit(), 1);
200 let limit = ConcurrencyLimit::new(2);
201 let live = Arc::new(AtomicUsize::new(0));
202 let peak = Arc::new(AtomicUsize::new(0));
203 let mut tasks = Vec::new();
204 for _ in 0..6 {
205 let (limit, live, peak) = (limit.clone(), live.clone(), peak.clone());
206 tasks.push(tokio::spawn(async move {
207 limit
208 .run(async {
209 let now = live.fetch_add(1, Ordering::SeqCst) + 1;
210 peak.fetch_max(now, Ordering::SeqCst);
211 #[allow(clippy::disallowed_methods)]
212 tokio::time::sleep(Duration::from_millis(5)).await;
213 live.fetch_sub(1, Ordering::SeqCst);
214 })
215 .await
216 }));
217 }
218 for t in tasks {
219 t.await.unwrap();
220 }
221 assert_eq!(peak.load(Ordering::SeqCst), 2);
222 }
223
224 #[tokio::test]
225 async fn the_first_error_stops_the_run() {
226 let r = for_each_concurrent(0..5, 2, |i| async move {
227 if i == 2 {
228 Err(FaucetError::Sink("boom".into()))
229 } else {
230 Ok(())
231 }
232 })
233 .await;
234 assert!(matches!(r, Err(FaucetError::Sink(m)) if m == "boom"));
235 let r: Result<Vec<u8>, _> =
236 map_concurrent(0..3u8, 1, |_| async { Err(FaucetError::Sink("x".into())) }).await;
237 assert!(r.is_err());
238 }
239}