1use std::future::Future;
5use std::future::poll_fn;
6use std::panic;
7use std::pin::pin;
8use std::sync::Arc;
9
10use futures::FutureExt;
11use futures::future::try_join_all;
12use futures::stream::Stream;
13use futures::try_join;
14use tokio::sync::mpsc;
15use tokio::task::JoinSet;
16
17#[derive(Debug, Clone)]
29pub struct ConcurrencyConfig {
30 pub initial: usize,
31 pub min: usize,
32 pub max: usize,
33 pub dead_band_low: f64,
36 pub dead_band_high: f64,
38}
39
40#[derive(Debug, Clone, Copy)]
42pub struct ConcurrencyStats {
43 pub limit: usize,
44 pub inflight: usize,
45}
46
47#[derive(Debug)]
50pub enum Break<E> {
51 Break,
52 Err(E),
53}
54
55pub trait TrySpawnStreamExt: Stream {
57 fn try_for_each_spawned<Fut, F, E>(
74 self,
75 limit: impl Into<Option<usize>>,
76 f: F,
77 ) -> impl Future<Output = Result<(), E>>
78 where
79 Fut: Future<Output = Result<(), E>> + Send + 'static,
80 F: FnMut(Self::Item) -> Fut,
81 E: Send + 'static;
82
83 fn try_for_each_send_spawned<Fut, F, T, E, R>(
95 self,
96 config: ConcurrencyConfig,
97 f: F,
98 tx: mpsc::Sender<T>,
99 report: R,
100 ) -> impl Future<Output = Result<(), Break<E>>>
101 where
102 Fut: Future<Output = Result<T, Break<E>>> + Send + 'static,
103 F: FnMut(Self::Item) -> Fut,
104 T: Send + 'static,
105 E: Send + 'static,
106 R: Fn(ConcurrencyStats);
107
108 fn try_for_each_broadcast_spawned<Fut, F, T, E, R>(
114 self,
115 config: ConcurrencyConfig,
116 f: F,
117 txs: Vec<mpsc::Sender<T>>,
118 report: R,
119 ) -> impl Future<Output = Result<(), Break<E>>>
120 where
121 Fut: Future<Output = Result<T, Break<E>>> + Send + 'static,
122 F: FnMut(Self::Item) -> Fut,
123 T: Clone + Send + Sync + 'static,
124 E: Send + 'static,
125 R: Fn(ConcurrencyStats);
126}
127
128trait Sender: Clone + Send + Sync + 'static {
134 type Value: Send + 'static;
135
136 fn send(&self, value: Self::Value) -> impl Future<Output = Result<(), ()>> + Send;
138
139 fn fill(&self) -> f64;
142}
143
144struct SingleSender<T>(mpsc::Sender<T>);
146
147struct BroadcastSender<T>(Arc<Vec<mpsc::Sender<T>>>);
149
150impl ConcurrencyConfig {
151 pub fn fixed(n: usize) -> Self {
152 Self {
153 initial: n,
154 min: n,
155 max: n,
156 dead_band_low: 0.6,
157 dead_band_high: 0.85,
158 }
159 }
160
161 pub fn adaptive(initial: usize, min: usize, max: usize) -> Self {
162 Self {
163 initial,
164 min,
165 max,
166 dead_band_low: 0.6,
167 dead_band_high: 0.85,
168 }
169 }
170
171 pub fn with_dead_band(mut self, low: f64, high: f64) -> Self {
172 self.dead_band_low = low;
173 self.dead_band_high = high;
174 self
175 }
176}
177
178impl<E> From<E> for Break<E> {
179 fn from(e: E) -> Self {
180 Break::Err(e)
181 }
182}
183
184impl<S: Stream + Sized + 'static> TrySpawnStreamExt for S {
185 async fn try_for_each_spawned<Fut, F, E>(
186 self,
187 limit: impl Into<Option<usize>>,
188 mut f: F,
189 ) -> Result<(), E>
190 where
191 Fut: Future<Output = Result<(), E>> + Send + 'static,
192 F: FnMut(Self::Item) -> Fut,
193 E: Send + 'static,
194 {
195 let limit = match limit.into() {
197 Some(0) | None => usize::MAX,
198 Some(n) => n,
199 };
200
201 let mut permits = limit;
203 let mut join_set = JoinSet::new();
205 let mut draining = false;
207 let mut error = None;
209
210 let mut self_ = pin!(self);
211
212 loop {
213 while !draining && permits > 0 {
216 match poll_fn(|cx| self_.as_mut().poll_next(cx)).now_or_never() {
217 Some(Some(item)) => {
218 permits -= 1;
219 join_set.spawn(f(item).boxed());
220 }
221 Some(None) => {
222 draining = true;
226 }
227 None => break,
228 }
229 }
230
231 tokio::select! {
232 biased;
233
234 Some(res) = join_set.join_next() => {
235 match res {
236 Ok(Err(e)) if error.is_none() => {
237 error = Some(e);
238 permits += 1;
239 draining = true;
240 }
241
242 Ok(_) => permits += 1,
243
244 Err(e) if e.is_panic() => {
246 panic::resume_unwind(e.into_panic())
247 }
248
249 Err(e) => {
254 assert!(e.is_cancelled());
255 permits += 1;
256 draining = true;
257 }
258 }
259 }
260
261 next = poll_fn(|cx| self_.as_mut().poll_next(cx)),
262 if !draining && permits > 0 => {
263 if let Some(item) = next {
264 permits -= 1;
265 join_set.spawn(f(item).boxed());
266 } else {
267 draining = true;
268 }
269 }
270
271 else => {
272 if permits == limit && draining {
273 break;
274 }
275 }
276 }
277 }
278
279 if let Some(e) = error { Err(e) } else { Ok(()) }
280 }
281
282 async fn try_for_each_send_spawned<Fut, F, T, E, R>(
283 self,
284 config: ConcurrencyConfig,
285 f: F,
286 tx: mpsc::Sender<T>,
287 report: R,
288 ) -> Result<(), Break<E>>
289 where
290 Fut: Future<Output = Result<T, Break<E>>> + Send + 'static,
291 F: FnMut(Self::Item) -> Fut,
292 T: Send + 'static,
293 E: Send + 'static,
294 R: Fn(ConcurrencyStats),
295 {
296 adaptive_spawn_send(self, config, f, SingleSender(tx), report).await
297 }
298
299 async fn try_for_each_broadcast_spawned<Fut, F, T, E, R>(
300 self,
301 config: ConcurrencyConfig,
302 f: F,
303 txs: Vec<mpsc::Sender<T>>,
304 report: R,
305 ) -> Result<(), Break<E>>
306 where
307 Fut: Future<Output = Result<T, Break<E>>> + Send + 'static,
308 F: FnMut(Self::Item) -> Fut,
309 T: Clone + Send + Sync + 'static,
310 E: Send + 'static,
311 R: Fn(ConcurrencyStats),
312 {
313 adaptive_spawn_send(self, config, f, BroadcastSender(Arc::new(txs)), report).await
314 }
315}
316
317impl<T> Clone for SingleSender<T> {
318 fn clone(&self) -> Self {
319 Self(self.0.clone())
320 }
321}
322
323impl<T: Send + 'static> Sender for SingleSender<T> {
324 type Value = T;
325
326 async fn send(&self, value: T) -> Result<(), ()> {
327 self.0.send(value).await.map_err(|_| ())
328 }
329
330 fn fill(&self) -> f64 {
331 1.0 - (self.0.capacity() as f64 / self.0.max_capacity() as f64)
332 }
333}
334
335impl<T> Clone for BroadcastSender<T> {
336 fn clone(&self) -> Self {
337 Self(self.0.clone())
338 }
339}
340
341impl<T: Clone + Send + Sync + 'static> Sender for BroadcastSender<T> {
342 type Value = T;
343
344 async fn send(&self, value: T) -> Result<(), ()> {
345 let (last, rest) = self.0.split_last().ok_or(())?;
346 let rest_fut = try_join_all(rest.iter().map(|tx| {
347 let v = value.clone();
348 async move { tx.send(v).await.map_err(|_| ()) }
349 }));
350 let last_fut = last.send(value).map(|r| r.map_err(|_| ()));
351 try_join!(rest_fut, last_fut)?;
352 Ok(())
353 }
354
355 fn fill(&self) -> f64 {
356 self.0
357 .iter()
358 .map(|tx| 1.0 - (tx.capacity() as f64 / tx.max_capacity() as f64))
359 .fold(0.0f64, f64::max)
360 }
361}
362
363async fn adaptive_spawn_send<S, Fut, F, E, Tx, R>(
435 stream: S,
436 config: ConcurrencyConfig,
437 mut f: F,
438 sender: Tx,
439 report: R,
440) -> Result<(), Break<E>>
441where
442 S: Stream + 'static,
443 Fut: Future<Output = Result<Tx::Value, Break<E>>> + Send + 'static,
444 F: FnMut(S::Item) -> Fut,
445 E: Send + 'static,
446 Tx: Sender,
447 R: Fn(ConcurrencyStats),
448{
449 assert!(config.min >= 1, "ConcurrencyConfig::min must be >= 1");
450 let mut limit = config.initial;
451 let mut epoch: u64 = 0;
452 let mut was_saturated = false;
453 let mut tasks: JoinSet<Result<u64, Break<E>>> = JoinSet::new();
454 let mut stream_done = false;
455 let mut error: Option<Break<E>> = None;
456
457 let mut stream = pin!(stream);
458
459 loop {
460 if tasks.is_empty() && (stream_done || error.is_some()) {
461 break;
462 }
463
464 while tasks.len() < limit && !stream_done && error.is_none() {
466 match poll_fn(|cx| stream.as_mut().poll_next(cx)).now_or_never() {
467 Some(Some(item)) => {
468 let fut = f(item);
469 let tx = sender.clone();
470 let spawn_epoch = epoch;
471 tasks.spawn(
472 async move {
473 let value = fut.await?;
474 tx.send(value).await.map_err(|_| Break::Break)?;
475 Ok(spawn_epoch)
476 }
477 .boxed(),
478 );
479 if tasks.len() >= limit {
480 was_saturated = true;
481 }
482 }
483 Some(None) => stream_done = true,
484 None => break,
485 }
486 }
487
488 let completed = tokio::select! {
489 biased;
490
491 Some(r) = tasks.join_next(), if !tasks.is_empty() => Some(r),
492
493 next = poll_fn(|cx| stream.as_mut().poll_next(cx)),
494 if tasks.len() < limit && !stream_done && error.is_none() =>
495 {
496 if let Some(item) = next {
497 let fut = f(item);
498 let tx = sender.clone();
499 let spawn_epoch = epoch;
500 tasks.spawn(
501 async move {
502 let value = fut.await?;
503 tx.send(value).await.map_err(|_| Break::Break)?;
504 Ok(spawn_epoch)
505 }
506 .boxed(),
507 );
508 if tasks.len() >= limit {
509 was_saturated = true;
510 }
511 } else {
512 stream_done = true;
513 }
514 None
515 }
516
517 else => {
518 if tasks.is_empty() && (stream_done || error.is_some()) {
519 break;
520 }
521 None
522 }
523 };
524
525 for join_result in completed.into_iter().chain(std::iter::from_fn(|| {
527 tasks.join_next().now_or_never().flatten()
528 })) {
529 match join_result {
530 Ok(Ok(spawn_epoch)) => {
531 let fill = sender.fill();
537 if fill >= config.dead_band_high && spawn_epoch == epoch {
538 let severity =
543 (fill - config.dead_band_high) / (1.0 - config.dead_band_high);
544 let keep = 0.8 - 0.3 * severity;
545 let new_limit = ((limit as f64) * keep).ceil() as usize;
546 limit = new_limit.min(limit.saturating_sub(1)).max(config.min);
547 limit = limit.clamp(config.min, config.max);
548 epoch += 1;
549 was_saturated = false;
556 } else if fill < config.dead_band_low && was_saturated {
557 let increment = ((limit as f64).log10().ceil() as usize).max(1);
558 limit = (limit + increment).min(config.max);
559 was_saturated = false;
560 }
561 }
562 Ok(Err(e)) if error.is_none() => error = Some(e),
563 Ok(Err(_)) => {}
564 Err(e) if e.is_panic() => panic::resume_unwind(e.into_panic()),
565 Err(e) => {
566 assert!(e.is_cancelled());
567 stream_done = true;
568 }
569 }
570 }
571
572 report(ConcurrencyStats {
573 limit,
574 inflight: tasks.len(),
575 });
576 }
577
578 if let Some(e) = error { Err(e) } else { Ok(()) }
579}
580
581#[cfg(test)]
582mod tests {
583 use std::{
584 sync::{
585 Arc, Mutex,
586 atomic::{AtomicUsize, Ordering},
587 },
588 time::Duration,
589 };
590
591 use futures::stream;
592
593 use super::*;
594
595 #[tokio::test]
596 async fn for_each_explicit_sequential_iteration() {
597 let actual = Arc::new(Mutex::new(vec![]));
598 let result = stream::iter(0..20)
599 .try_for_each_spawned(1, |i| {
600 let actual = actual.clone();
601 async move {
602 tokio::time::sleep(Duration::from_millis(20 - i)).await;
603 actual.lock().unwrap().push(i);
604 Ok::<(), ()>(())
605 }
606 })
607 .await;
608
609 assert!(result.is_ok());
610
611 let actual = Arc::try_unwrap(actual).unwrap().into_inner().unwrap();
612 let expect: Vec<_> = (0..20).collect();
613 assert_eq!(expect, actual);
614 }
615
616 #[tokio::test]
617 async fn for_each_concurrent_iteration() {
618 let actual = Arc::new(AtomicUsize::new(0));
619 let result = stream::iter(0..100)
620 .try_for_each_spawned(16, |i| {
621 let actual = actual.clone();
622 async move {
623 actual.fetch_add(i, Ordering::Relaxed);
624 Ok::<(), ()>(())
625 }
626 })
627 .await;
628
629 assert!(result.is_ok());
630
631 let actual = Arc::try_unwrap(actual).unwrap().into_inner();
632 let expect = 99 * 100 / 2;
633 assert_eq!(expect, actual);
634 }
635
636 #[tokio::test]
637 async fn for_each_implicit_unlimited_iteration() {
638 let actual = Arc::new(AtomicUsize::new(0));
639 let result = stream::iter(0..100)
640 .try_for_each_spawned(None, |i| {
641 let actual = actual.clone();
642 async move {
643 actual.fetch_add(i, Ordering::Relaxed);
644 Ok::<(), ()>(())
645 }
646 })
647 .await;
648
649 assert!(result.is_ok());
650
651 let actual = Arc::try_unwrap(actual).unwrap().into_inner();
652 let expect = 99 * 100 / 2;
653 assert_eq!(expect, actual);
654 }
655
656 #[tokio::test]
657 async fn for_each_explicit_unlimited_iteration() {
658 let actual = Arc::new(AtomicUsize::new(0));
659 let result = stream::iter(0..100)
660 .try_for_each_spawned(0, |i| {
661 let actual = actual.clone();
662 async move {
663 actual.fetch_add(i, Ordering::Relaxed);
664 Ok::<(), ()>(())
665 }
666 })
667 .await;
668
669 assert!(result.is_ok());
670
671 let actual = Arc::try_unwrap(actual).unwrap().into_inner();
672 let expect = 99 * 100 / 2;
673 assert_eq!(expect, actual);
674 }
675
676 #[tokio::test]
677 async fn for_each_max_concurrency() {
678 #[derive(Default, Debug)]
679 struct Jobs {
680 max: AtomicUsize,
681 curr: AtomicUsize,
682 }
683
684 let jobs = Arc::new(Jobs::default());
685
686 let result = stream::iter(0..32)
687 .try_for_each_spawned(4, |_| {
688 let jobs = jobs.clone();
689 async move {
690 jobs.curr.fetch_add(1, Ordering::Relaxed);
691 tokio::time::sleep(Duration::from_millis(100)).await;
692 let prev = jobs.curr.fetch_sub(1, Ordering::Relaxed);
693 jobs.max.fetch_max(prev, Ordering::Relaxed);
694 Ok::<(), ()>(())
695 }
696 })
697 .await;
698
699 assert!(result.is_ok());
700
701 let Jobs { max, curr } = Arc::try_unwrap(jobs).unwrap();
702 assert_eq!(curr.into_inner(), 0);
703 assert!(max.into_inner() <= 4);
704 }
705
706 #[tokio::test]
707 async fn for_each_error_propagation() {
708 let actual = Arc::new(Mutex::new(vec![]));
709 let result = stream::iter(0..100)
710 .try_for_each_spawned(None, |i| {
711 let actual = actual.clone();
712 async move {
713 if i < 42 {
714 actual.lock().unwrap().push(i);
715 Ok(())
716 } else {
717 Err(())
718 }
719 }
720 })
721 .await;
722
723 assert!(result.is_err());
724
725 let actual = Arc::try_unwrap(actual).unwrap().into_inner().unwrap();
726 let expect: Vec<_> = (0..42).collect();
727 assert_eq!(expect, actual);
728 }
729
730 #[tokio::test]
731 #[should_panic]
732 async fn for_each_panic_propagation() {
733 let _ = stream::iter(0..100)
734 .try_for_each_spawned(None, |i| async move {
735 assert!(i < 42);
736 Ok::<(), ()>(())
737 })
738 .await;
739 }
740
741 #[tokio::test]
742 async fn send_spawned_basic() {
743 let (tx, mut rx) = mpsc::channel(100);
744 let result = stream::iter(0..10u64)
745 .try_for_each_send_spawned(
746 ConcurrencyConfig::fixed(4),
747 |i| async move { Ok::<_, Break<()>>(i * 2) },
748 tx,
749 |_| {},
750 )
751 .await;
752
753 assert!(result.is_ok());
754
755 let mut values = Vec::new();
756 while let Ok(v) = rx.try_recv() {
757 values.push(v);
758 }
759 values.sort();
760 let expected: Vec<u64> = (0..10).map(|i| i * 2).collect();
761 assert_eq!(values, expected);
762 }
763
764 #[tokio::test]
765 async fn send_spawned_error_propagation() {
766 let (tx, _rx) = mpsc::channel(100);
767 let result: Result<(), Break<String>> = stream::iter(0..10u64)
768 .try_for_each_send_spawned(
769 ConcurrencyConfig::fixed(1),
770 |i| async move {
771 if i < 3 {
772 Ok(i)
773 } else {
774 Err(Break::Err("fail".to_string()))
775 }
776 },
777 tx,
778 |_| {},
779 )
780 .await;
781
782 assert!(matches!(result, Err(Break::Err(ref s)) if s == "fail"));
783 }
784
785 #[tokio::test]
786 async fn send_spawned_channel_closed() {
787 let (tx, rx) = mpsc::channel(1);
788 drop(rx);
789
790 let result: Result<(), Break<()>> = stream::iter(0..10u64)
791 .try_for_each_send_spawned(
792 ConcurrencyConfig::fixed(1),
793 |i| async move { Ok(i) },
794 tx,
795 |_| {},
796 )
797 .await;
798
799 assert!(matches!(result, Err(Break::Break)));
800 }
801
802 #[tokio::test]
803 async fn send_spawned_reports_stats() {
804 let reported: Arc<Mutex<Vec<ConcurrencyStats>>> = Arc::new(Mutex::new(Vec::new()));
805 let (tx, _rx) = mpsc::channel(100);
806
807 let reported2 = reported.clone();
808 let _ = stream::iter(0..5u64)
809 .try_for_each_send_spawned(
810 ConcurrencyConfig::fixed(2),
811 |i| async move { Ok::<_, Break<()>>(i) },
812 tx,
813 move |stats| {
814 reported2.lock().unwrap().push(stats);
815 },
816 )
817 .await;
818
819 let reports = reported.lock().unwrap();
820 for stats in reports.iter() {
821 assert_eq!(stats.limit, 2);
822 }
823 }
824
825 #[tokio::test]
826 async fn broadcast_spawned_basic() {
827 let (tx1, mut rx1) = mpsc::channel(100);
828 let (tx2, mut rx2) = mpsc::channel(100);
829 let txs = vec![tx1, tx2];
830
831 let result = stream::iter(0..5u64)
832 .try_for_each_broadcast_spawned(
833 ConcurrencyConfig::fixed(2),
834 |i| async move { Ok::<_, Break<()>>(i * 3) },
835 txs,
836 |_| {},
837 )
838 .await;
839
840 assert!(result.is_ok());
841
842 let mut v1 = Vec::new();
843 while let Ok(v) = rx1.try_recv() {
844 v1.push(v);
845 }
846 let mut v2 = Vec::new();
847 while let Ok(v) = rx2.try_recv() {
848 v2.push(v);
849 }
850 v1.sort();
851 v2.sort();
852 let expected: Vec<u64> = (0..5).map(|i| i * 3).collect();
853 assert_eq!(v1, expected);
854 assert_eq!(v2, expected);
855 }
856
857 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
858 async fn send_spawned_adaptive_decreases_limit() {
859 let (tx, mut rx) = mpsc::channel(4);
861 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
862
863 let limits2 = limits.clone();
864 let handle = tokio::spawn(async move {
865 stream::iter(0..100u64)
866 .try_for_each_send_spawned(
867 ConcurrencyConfig::adaptive(10, 1, 20),
868 |i| async move {
869 tokio::time::sleep(Duration::from_millis(5)).await;
871 Ok::<_, Break<()>>(i)
872 },
873 tx,
874 move |stats| {
875 limits2.lock().unwrap().push(stats.limit);
876 },
877 )
878 .await
879 });
880
881 let mut received = Vec::new();
883 loop {
884 tokio::time::sleep(Duration::from_millis(20)).await;
885 match rx.try_recv() {
886 Ok(v) => received.push(v),
887 Err(mpsc::error::TryRecvError::Empty) => {
888 if handle.is_finished() {
889 while let Ok(v) = rx.try_recv() {
891 received.push(v);
892 }
893 break;
894 }
895 }
896 Err(mpsc::error::TryRecvError::Disconnected) => {
897 while let Ok(v) = rx.try_recv() {
898 received.push(v);
899 }
900 break;
901 }
902 }
903 }
904
905 handle.await.unwrap().unwrap();
906
907 let limits = limits.lock().unwrap();
908 let min_limit = limits.iter().copied().min().unwrap_or(10);
909 assert!(
910 min_limit < 10,
911 "Limit should have decreased from initial=10, min observed: {min_limit}"
912 );
913 }
914
915 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
916 async fn send_spawned_adaptive_recovers_after_decrease() {
917 let (tx, mut rx) = mpsc::channel(4);
918 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
919
920 let limits2 = limits.clone();
921 let handle = tokio::spawn(async move {
922 stream::iter(0..200u64)
923 .try_for_each_send_spawned(
924 ConcurrencyConfig::adaptive(10, 1, 20),
925 |i| async move {
926 tokio::time::sleep(Duration::from_millis(5)).await;
927 Ok::<_, Break<()>>(i)
928 },
929 tx,
930 move |stats| {
931 limits2.lock().unwrap().push(stats.limit);
932 },
933 )
934 .await
935 });
936
937 for _ in 0..60 {
939 tokio::time::sleep(Duration::from_millis(20)).await;
940 let _ = rx.try_recv();
941 }
942
943 let low_water = {
945 let lims = limits.lock().unwrap();
946 lims.iter().copied().min().unwrap_or(10)
947 };
948 assert!(
949 low_water < 10,
950 "Limit should have decreased, min={low_water}"
951 );
952
953 while (rx.recv().await).is_some() {
955 if handle.is_finished() {
956 while rx.try_recv().is_ok() {}
957 break;
958 }
959 }
960
961 handle.await.unwrap().unwrap();
962
963 let limits = limits.lock().unwrap();
964 let recovered_max = limits.iter().copied().rev().take(30).max().unwrap_or(0);
965 assert!(
966 recovered_max > low_water,
967 "Limit should have recovered above {low_water}, best late limit: {recovered_max}"
968 );
969 }
970
971 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
972 async fn send_spawned_adaptive_respects_min() {
973 let (tx, mut rx) = mpsc::channel(2);
974 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
975
976 let limits2 = limits.clone();
977 let handle = tokio::spawn(async move {
978 stream::iter(0..100u64)
979 .try_for_each_send_spawned(
980 ConcurrencyConfig::adaptive(10, 5, 20),
981 |i| async move {
982 tokio::time::sleep(Duration::from_millis(10)).await;
983 Ok::<_, Break<()>>(i)
984 },
985 tx,
986 move |stats| {
987 limits2.lock().unwrap().push(stats.limit);
988 },
989 )
990 .await
991 });
992
993 loop {
995 tokio::time::sleep(Duration::from_millis(50)).await;
996 match rx.try_recv() {
997 Ok(_) => {}
998 Err(mpsc::error::TryRecvError::Empty) => {
999 if handle.is_finished() {
1000 while rx.try_recv().is_ok() {}
1001 break;
1002 }
1003 }
1004 Err(mpsc::error::TryRecvError::Disconnected) => {
1005 while rx.try_recv().is_ok() {}
1006 break;
1007 }
1008 }
1009 }
1010
1011 handle.await.unwrap().unwrap();
1012
1013 let limits = limits.lock().unwrap();
1014 let min_limit = limits.iter().copied().min().unwrap_or(10);
1015 assert!(
1016 min_limit >= 5,
1017 "Limit should never drop below min=5, observed: {min_limit}"
1018 );
1019 }
1020
1021 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1022 async fn send_spawned_adaptive_respects_max() {
1023 let (tx, mut rx) = mpsc::channel(1000);
1024 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
1025
1026 let limits2 = limits.clone();
1027 let handle = tokio::spawn(async move {
1028 stream::iter(0..200u64)
1029 .try_for_each_send_spawned(
1030 ConcurrencyConfig::adaptive(2, 1, 8),
1031 |i| async move {
1032 tokio::time::sleep(Duration::from_millis(5)).await;
1033 Ok::<_, Break<()>>(i)
1034 },
1035 tx,
1036 move |stats| {
1037 limits2.lock().unwrap().push(stats.limit);
1038 },
1039 )
1040 .await
1041 });
1042
1043 while rx.recv().await.is_some() {}
1045
1046 handle.await.unwrap().unwrap();
1047
1048 let limits = limits.lock().unwrap();
1049 let max_limit = limits.iter().copied().max().unwrap_or(0);
1050 assert!(
1051 max_limit <= 8,
1052 "Limit should never exceed max=8, observed: {max_limit}"
1053 );
1054 }
1055
1056 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1057 async fn send_spawned_epoch_prevents_stampede() {
1058 let (tx, mut rx) = mpsc::channel(2);
1059 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
1060
1061 let limits2 = limits.clone();
1062 let handle = tokio::spawn(async move {
1063 stream::iter(0..60u64)
1064 .try_for_each_send_spawned(
1065 ConcurrencyConfig::adaptive(20, 1, 20),
1066 |i| async move {
1067 tokio::time::sleep(Duration::from_millis(10)).await;
1068 Ok::<_, Break<()>>(i)
1069 },
1070 tx,
1071 move |stats| {
1072 limits2.lock().unwrap().push(stats.limit);
1073 },
1074 )
1075 .await
1076 });
1077
1078 tokio::time::sleep(Duration::from_millis(300)).await;
1081
1082 while rx.recv().await.is_some() {}
1084 handle.await.unwrap().unwrap();
1085
1086 let limits = limits.lock().unwrap();
1087 let transitions: Vec<usize> = limits
1089 .iter()
1090 .copied()
1091 .collect::<Vec<_>>()
1092 .windows(2)
1093 .filter_map(|w| if w[0] != w[1] { Some(w[1]) } else { None })
1094 .collect();
1095
1096 for pair in limits.iter().copied().collect::<Vec<_>>().windows(2) {
1099 let (old, new) = (pair[0], pair[1]);
1100 if new < old {
1101 let min_allowed = ((old as f64) * 0.5).ceil() as usize;
1102 assert!(
1103 new >= min_allowed,
1104 "Stampede detected: limit dropped from {old} to {new}, \
1105 minimum allowed single-step is {min_allowed}. Transitions: {transitions:?}"
1106 );
1107 }
1108 }
1109 }
1110
1111 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1112 async fn broadcast_spawned_slow_receiver_triggers_decrease() {
1113 let (tx_fast, mut rx_fast) = mpsc::channel(100);
1114 let (tx_slow, mut rx_slow) = mpsc::channel(4);
1115 let txs = vec![tx_fast, tx_slow];
1116 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
1117
1118 let limits2 = limits.clone();
1119 let handle = tokio::spawn(async move {
1120 stream::iter(0..100u64)
1121 .try_for_each_broadcast_spawned(
1122 ConcurrencyConfig::adaptive(10, 1, 20),
1123 |i| async move {
1124 tokio::time::sleep(Duration::from_millis(5)).await;
1125 Ok::<_, Break<()>>(i)
1126 },
1127 txs,
1128 move |stats| {
1129 limits2.lock().unwrap().push(stats.limit);
1130 },
1131 )
1132 .await
1133 });
1134
1135 let fast_drain = tokio::spawn(async move { while rx_fast.recv().await.is_some() {} });
1137
1138 loop {
1140 tokio::time::sleep(Duration::from_millis(20)).await;
1141 match rx_slow.try_recv() {
1142 Ok(_) => {}
1143 Err(mpsc::error::TryRecvError::Empty) => {
1144 if handle.is_finished() {
1145 while rx_slow.try_recv().is_ok() {}
1146 break;
1147 }
1148 }
1149 Err(mpsc::error::TryRecvError::Disconnected) => {
1150 while rx_slow.try_recv().is_ok() {}
1151 break;
1152 }
1153 }
1154 }
1155
1156 handle.await.unwrap().unwrap();
1157 fast_drain.await.unwrap();
1158
1159 let limits = limits.lock().unwrap();
1160 let min_limit = limits.iter().copied().min().unwrap_or(10);
1161 assert!(
1162 min_limit < 10,
1163 "Limit should have decreased due to slow receiver, min observed: {min_limit}"
1164 );
1165 }
1166
1167 #[tokio::test]
1168 async fn broadcast_spawned_channel_closed() {
1169 let (tx1, _rx1) = mpsc::channel(100);
1170 let (tx2, rx2) = mpsc::channel(100);
1171 drop(rx2);
1172
1173 let result: Result<(), Break<()>> = stream::iter(0..10u64)
1174 .try_for_each_broadcast_spawned(
1175 ConcurrencyConfig::fixed(2),
1176 |i| async move { Ok(i) },
1177 vec![tx1, tx2],
1178 |_| {},
1179 )
1180 .await;
1181
1182 assert!(matches!(result, Err(Break::Break)));
1183 }
1184
1185 #[tokio::test]
1186 async fn fixed_concurrency_limit_never_changes() {
1187 let limits: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
1188 let (tx, mut rx) = mpsc::channel(2);
1189
1190 let limits2 = limits.clone();
1191 let handle = tokio::spawn(async move {
1192 stream::iter(0..20u64)
1193 .try_for_each_send_spawned(
1194 ConcurrencyConfig::fixed(5),
1195 |i| async move { Ok::<_, Break<()>>(i) },
1196 tx,
1197 move |stats| {
1198 limits2.lock().unwrap().push(stats.limit);
1199 },
1200 )
1201 .await
1202 });
1203
1204 while rx.recv().await.is_some() {}
1206
1207 handle.await.unwrap().unwrap();
1208
1209 let limits = limits.lock().unwrap();
1210 for &g in limits.iter() {
1211 assert_eq!(g, 5, "Fixed limit should never change");
1212 }
1213 }
1214}