futures-bounded-0.3.0/.cargo_vcs_info.json0000644000000001360000000000100141470ustar { "git": { "sha1": "d0fc40e0fe6c12b92696f766db6b62fd8f9051bc" }, "path_in_vcs": "" }futures-bounded-0.3.0/.github/workflows/CI.yml000064400000000000000000000005151046102023000173530ustar 00000000000000name: CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo build --verbose - run: cargo test --verbose --all-features futures-bounded-0.3.0/.gitignore000064400000000000000000000000071046102023000147240ustar 00000000000000target/futures-bounded-0.3.0/CHANGELOG.md000064400000000000000000000020001046102023000145400ustar 00000000000000## 0.3.0 - Allow for multiple timer implementations. See [PR 5](https://github.com/thomaseizinger/rust-futures-bounded/pull/5). ## 0.2.4 - Add `FuturesMap.contains()`. See [PR 3](https://github.com/thomaseizinger/rust-futures-bounded/pull/3). ## 0.2.3 - Introduce `FuturesTupleSet`, holding tuples of a `Future` together with an arbitrary piece of data. See [PR 4841](https://github.com/libp2p/rust-libp2p/pull/4841). ## 0.2.2 - Fix an issue where `{Futures,Stream}Map` returns `Poll::Pending` despite being ready after an item has been replaced as part of `try_push`. See [PR 4865](https://github.com/libp2p/rust-libp2p/pull/4865). ## 0.2.1 - Add `.len()` getter to `FuturesMap`, `FuturesSet`, `StreamMap` and `StreamSet`. See [PR 4745](https://github.com/libp2p/rust-libp2p/pull/4745). ## 0.2.0 - Add `StreamMap` type and remove `Future`-suffix from `PushError::ReplacedFuture` to reuse it for `StreamMap`. See [PR 4616](https://github.com/libp2p/rust-libp2p/pull/4616). ## 0.1.0 Initial release. futures-bounded-0.3.0/Cargo.lock0000644000000160730000000000100121310ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "cc" version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-bounded" version = "0.3.0" dependencies = [ "futures", "futures-timer", "futures-util", "tokio", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "pin-project-lite", "tokio-macros", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" futures-bounded-0.3.0/Cargo.toml0000644000000024630000000000100121520ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "futures-bounded" version = "0.3.0" build = false publish = true autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Utilities for bounding futures in size and time." readme = false keywords = [ "futures", "async", "backpressure", ] categories = [ "data-structures", "asynchronous", ] license = "MIT" repository = "https://github.com/thomaseizinger/rust-futures-bounded" [lib] name = "futures_bounded" path = "src/lib.rs" [dependencies.futures-timer] version = "3.0.2" optional = true [dependencies.futures-util] version = "0.3.30" [dependencies.tokio] version = "1.35.1" features = ["time"] optional = true [dev-dependencies.futures] version = "0.3.30" [dev-dependencies.tokio] version = "1.35.1" features = [ "macros", "rt", "sync", ] futures-bounded-0.3.0/Cargo.toml.orig000064400000000000000000000013141046102023000156250ustar 00000000000000[package] name = "futures-bounded" version = "0.3.0" edition = "2021" license = "MIT" repository = "https://github.com/thomaseizinger/rust-futures-bounded" keywords = ["futures", "async", "backpressure"] categories = ["data-structures", "asynchronous"] description = "Utilities for bounding futures in size and time." publish = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] futures-util = { version = "0.3.30" } futures-timer = { version = "3.0.2", optional = true } tokio = { version = "1.35.1", features = ["time"], optional = true } [dev-dependencies] tokio = { version = "1.35.1", features = ["macros", "rt", "sync"] } futures = "0.3.30" futures-bounded-0.3.0/src/delay.rs000064400000000000000000000016441046102023000151770ustar 00000000000000use std::future::Future; use std::pin::Pin; use std::task::{ready, Context, Poll}; use std::time::Duration; use futures_util::future::BoxFuture; use futures_util::FutureExt as _; pub struct Delay { duration: Duration, inner: BoxFuture<'static, ()>, } impl Delay { #[cfg(feature = "tokio")] pub fn tokio(duration: Duration) -> Self { Self { duration, inner: tokio::time::sleep(duration).boxed(), } } #[cfg(feature = "futures-timer")] pub fn futures_timer(duration: Duration) -> Self { Self { duration, inner: futures_timer::Delay::new(duration).boxed(), } } } impl Future for Delay { type Output = Duration; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); ready!(this.inner.poll_unpin(cx)); Poll::Ready(this.duration) } } futures-bounded-0.3.0/src/futures_map.rs000064400000000000000000000240501046102023000164270ustar 00000000000000use std::future::Future; use std::hash::Hash; use std::pin::Pin; use std::task::{Context, Poll, Waker}; use std::time::Duration; use std::{future, mem}; use futures_util::future::BoxFuture; use futures_util::stream::FuturesUnordered; use futures_util::{FutureExt, StreamExt}; use crate::{Delay, PushError, Timeout}; /// Represents a map of [`Future`]s. /// /// Each future must finish within the specified time and the map never outgrows its capacity. pub struct FuturesMap { make_delay: Box Delay + Send + Sync>, capacity: usize, inner: FuturesUnordered>>>, empty_waker: Option, full_waker: Option, } impl FuturesMap { pub fn new(make_delay: impl Fn() -> Delay + Send + Sync + 'static, capacity: usize) -> Self { Self { make_delay: Box::new(make_delay), capacity, inner: Default::default(), empty_waker: None, full_waker: None, } } } impl FuturesMap where ID: Clone + Hash + Eq + Send + Unpin + 'static, O: 'static, { /// Push a future into the map. /// /// This method inserts the given future with defined `future_id` to the set. /// If the length of the map is equal to the capacity, this method returns [PushError::BeyondCapacity], /// that contains the passed future. In that case, the future is not inserted to the map. /// If a future with the given `future_id` already exists, then the old future will be replaced by a new one. /// In that case, the returned error [PushError::Replaced] contains the old future. pub fn try_push(&mut self, future_id: ID, future: F) -> Result<(), PushError>> where F: Future + Send + 'static, { if self.inner.len() >= self.capacity { return Err(PushError::BeyondCapacity(future.boxed())); } if let Some(waker) = self.empty_waker.take() { waker.wake(); } let old = self.remove(future_id.clone()); self.inner.push(TaggedFuture { tag: future_id, inner: TimeoutFuture { inner: future.boxed(), timeout: (self.make_delay)(), cancelled: false, }, }); match old { None => Ok(()), Some(old) => Err(PushError::Replaced(old)), } } pub fn remove(&mut self, id: ID) -> Option> { let tagged = self.inner.iter_mut().find(|s| s.tag == id)?; let inner = mem::replace(&mut tagged.inner.inner, future::pending().boxed()); tagged.inner.cancelled = true; Some(inner) } pub fn contains(&self, id: ID) -> bool { self.inner.iter().any(|f| f.tag == id && !f.inner.cancelled) } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } #[allow(unknown_lints, clippy::needless_pass_by_ref_mut)] // &mut Context is idiomatic. pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { if self.inner.len() < self.capacity { return Poll::Ready(()); } self.full_waker = Some(cx.waker().clone()); Poll::Pending } pub fn poll_unpin(&mut self, cx: &mut Context<'_>) -> Poll<(ID, Result)> { loop { let maybe_result = futures_util::ready!(self.inner.poll_next_unpin(cx)); match maybe_result { None => { self.empty_waker = Some(cx.waker().clone()); return Poll::Pending; } Some((id, Ok(output))) => return Poll::Ready((id, Ok(output))), Some((id, Err(TimeoutError::Timeout(dur)))) => { return Poll::Ready((id, Err(Timeout::new(dur)))) } Some((_, Err(TimeoutError::Cancelled))) => continue, } } } } struct TimeoutFuture { inner: F, timeout: Delay, cancelled: bool, } impl Future for TimeoutFuture where F: Future + Unpin, { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if self.cancelled { return Poll::Ready(Err(TimeoutError::Cancelled)); } if let Poll::Ready(duration) = self.timeout.poll_unpin(cx) { return Poll::Ready(Err(TimeoutError::Timeout(duration))); } self.inner.poll_unpin(cx).map(Ok) } } enum TimeoutError { Timeout(Duration), Cancelled, } struct TaggedFuture { tag: T, inner: F, } impl Future for TaggedFuture where T: Clone + Unpin, F: Future + Unpin, { type Output = (T, F::Output); fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let output = futures_util::ready!(self.inner.poll_unpin(cx)); Poll::Ready((self.tag.clone(), output)) } } #[cfg(all(test, feature = "futures-timer"))] mod tests { use futures::channel::oneshot; use futures_util::task::noop_waker_ref; use std::future::{pending, poll_fn, ready}; use std::pin::Pin; use std::time::Instant; use super::*; #[test] fn cannot_push_more_than_capacity_tasks() { let mut futures = FuturesMap::new(|| Delay::futures_timer(Duration::from_secs(10)), 1); assert!(futures.try_push("ID_1", ready(())).is_ok()); matches!( futures.try_push("ID_2", ready(())), Err(PushError::BeyondCapacity(_)) ); } #[test] fn cannot_push_the_same_id_few_times() { let mut futures = FuturesMap::new(|| Delay::futures_timer(Duration::from_secs(10)), 5); assert!(futures.try_push("ID", ready(())).is_ok()); matches!( futures.try_push("ID", ready(())), Err(PushError::Replaced(_)) ); } #[tokio::test] async fn futures_timeout() { let mut futures = FuturesMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 1); let _ = futures.try_push("ID", pending::<()>()); futures_timer::Delay::new(Duration::from_millis(150)).await; let (_, result) = poll_fn(|cx| futures.poll_unpin(cx)).await; assert!(result.is_err()) } #[test] fn resources_of_removed_future_are_cleaned_up() { let mut futures = FuturesMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 1); let _ = futures.try_push("ID", pending::<()>()); futures.remove("ID"); let poll = futures.poll_unpin(&mut Context::from_waker(noop_waker_ref())); assert!(poll.is_pending()); assert_eq!(futures.len(), 0); } #[tokio::test] async fn replaced_pending_future_is_polled() { let mut streams = FuturesMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 3); let (_tx1, rx1) = oneshot::channel(); let (tx2, rx2) = oneshot::channel(); let _ = streams.try_push("ID1", rx1); let _ = streams.try_push("ID2", rx2); let _ = tx2.send(2); let (id, res) = poll_fn(|cx| streams.poll_unpin(cx)).await; assert_eq!(id, "ID2"); assert_eq!(res.unwrap().unwrap(), 2); let (new_tx1, new_rx1) = oneshot::channel(); let replaced = streams.try_push("ID1", new_rx1); assert!(matches!(replaced.unwrap_err(), PushError::Replaced(_))); let _ = new_tx1.send(4); let (id, res) = poll_fn(|cx| streams.poll_unpin(cx)).await; assert_eq!(id, "ID1"); assert_eq!(res.unwrap().unwrap(), 4); } // Each future causes a delay, `Task` only has a capacity of 1, meaning they must be processed in sequence. // We stop after NUM_FUTURES tasks, meaning the overall execution must at least take DELAY * NUM_FUTURES. #[tokio::test] async fn backpressure() { const DELAY: Duration = Duration::from_millis(100); const NUM_FUTURES: u32 = 10; let start = Instant::now(); Task::new(DELAY, NUM_FUTURES, 1).await; let duration = start.elapsed(); assert!(duration >= DELAY * NUM_FUTURES); } #[test] fn contains() { let mut futures = FuturesMap::new(|| Delay::futures_timer(Duration::from_secs(10)), 1); _ = futures.try_push("ID", pending::<()>()); assert!(futures.contains("ID")); _ = futures.remove("ID"); assert!(!futures.contains("ID")); } struct Task { future: Duration, num_futures: usize, num_processed: usize, inner: FuturesMap, } impl Task { fn new(future: Duration, num_futures: u32, capacity: usize) -> Self { Self { future, num_futures: num_futures as usize, num_processed: 0, inner: FuturesMap::new(|| Delay::futures_timer(Duration::from_secs(60)), capacity), } } } impl Future for Task { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); while this.num_processed < this.num_futures { if let Poll::Ready((_, result)) = this.inner.poll_unpin(cx) { if result.is_err() { panic!("Timeout is great than future delay") } this.num_processed += 1; continue; } if let Poll::Ready(()) = this.inner.poll_ready_unpin(cx) { // We push the constant future's ID to prove that user can use the same ID // if the future was finished let maybe_future = this .inner .try_push(1u8, futures_timer::Delay::new(this.future)); assert!(maybe_future.is_ok(), "we polled for readiness"); continue; } return Poll::Pending; } Poll::Ready(()) } } } futures-bounded-0.3.0/src/futures_set.rs000064400000000000000000000033631046102023000164510ustar 00000000000000use std::future::Future; use std::task::{ready, Context, Poll}; use futures_util::future::BoxFuture; use crate::{Delay, FuturesMap, PushError, Timeout}; /// Represents a list of [Future]s. /// /// Each future must finish within the specified time and the list never outgrows its capacity. pub struct FuturesSet { id: u32, inner: FuturesMap, } impl FuturesSet { pub fn new(make_delay: impl Fn() -> Delay + Send + Sync + 'static, capacity: usize) -> Self { Self { id: 0, inner: FuturesMap::new(make_delay, capacity), } } } impl FuturesSet where O: 'static, { /// Push a future into the list. /// /// This method adds the given future to the list. /// If the length of the list is equal to the capacity, this method returns a error that contains the passed future. /// In that case, the future is not added to the set. pub fn try_push(&mut self, future: F) -> Result<(), BoxFuture> where F: Future + Send + 'static, { self.id = self.id.wrapping_add(1); match self.inner.try_push(self.id, future) { Ok(()) => Ok(()), Err(PushError::BeyondCapacity(w)) => Err(w), Err(PushError::Replaced(_)) => unreachable!("we never reuse IDs"), } } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { self.inner.poll_ready_unpin(cx) } pub fn poll_unpin(&mut self, cx: &mut Context<'_>) -> Poll> { let (_, res) = ready!(self.inner.poll_unpin(cx)); Poll::Ready(res) } } futures-bounded-0.3.0/src/futures_tuple_set.rs000064400000000000000000000053561046102023000176660ustar 00000000000000use std::collections::HashMap; use std::future::Future; use std::task::{ready, Context, Poll}; use futures_util::future::BoxFuture; use crate::{Delay, FuturesMap, PushError, Timeout}; /// Represents a list of tuples of a [Future] and an associated piece of data. /// /// Each future must finish within the specified time and the list never outgrows its capacity. pub struct FuturesTupleSet { id: u32, inner: FuturesMap, data: HashMap, } impl FuturesTupleSet { pub fn new(make_delay: impl Fn() -> Delay + Send + Sync + 'static, capacity: usize) -> Self { Self { id: 0, inner: FuturesMap::new(make_delay, capacity), data: HashMap::new(), } } } impl FuturesTupleSet where O: 'static, { /// Push a future into the list. /// /// This method adds the given future to the list. /// If the length of the list is equal to the capacity, this method returns a error that contains the passed future. /// In that case, the future is not added to the set. pub fn try_push(&mut self, future: F, data: D) -> Result<(), (BoxFuture, D)> where F: Future + Send + 'static, { self.id = self.id.wrapping_add(1); match self.inner.try_push(self.id, future) { Ok(()) => {} Err(PushError::BeyondCapacity(w)) => return Err((w, data)), Err(PushError::Replaced(_)) => unreachable!("we never reuse IDs"), } self.data.insert(self.id, data); Ok(()) } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { self.inner.poll_ready_unpin(cx) } pub fn poll_unpin(&mut self, cx: &mut Context<'_>) -> Poll<(Result, D)> { let (id, res) = ready!(self.inner.poll_unpin(cx)); let data = self.data.remove(&id).expect("must have data for future"); Poll::Ready((res, data)) } } #[cfg(test)] mod tests { use super::*; use futures_util::future::poll_fn; use futures_util::FutureExt; use std::future::ready; use std::time::Duration; #[test] fn tracks_associated_data_of_future() { let mut set = FuturesTupleSet::new(|| Delay::futures_timer(Duration::from_secs(10)), 10); let _ = set.try_push(ready(1), 1); let _ = set.try_push(ready(2), 2); let (res1, data1) = poll_fn(|cx| set.poll_unpin(cx)).now_or_never().unwrap(); let (res2, data2) = poll_fn(|cx| set.poll_unpin(cx)).now_or_never().unwrap(); assert_eq!(res1.unwrap(), data1); assert_eq!(res2.unwrap(), data2); } } futures-bounded-0.3.0/src/lib.rs000064400000000000000000000020521046102023000146410ustar 00000000000000mod delay; mod futures_map; mod futures_set; mod futures_tuple_set; mod stream_map; mod stream_set; pub use delay::Delay; pub use futures_map::FuturesMap; pub use futures_set::FuturesSet; pub use futures_tuple_set::FuturesTupleSet; pub use stream_map::StreamMap; pub use stream_set::StreamSet; use std::fmt; use std::fmt::Formatter; use std::time::Duration; /// A future failed to complete within the given timeout. #[derive(Debug)] pub struct Timeout { limit: Duration, } impl Timeout { fn new(duration: Duration) -> Self { Self { limit: duration } } } impl fmt::Display for Timeout { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "future failed to complete within {:?}", self.limit) } } /// Error of a future pushing #[derive(PartialEq, Debug)] pub enum PushError { /// The length of the set is equal to the capacity BeyondCapacity(T), /// The map already contained an item with this key. /// /// The old item is returned. Replaced(T), } impl std::error::Error for Timeout {} futures-bounded-0.3.0/src/stream_map.rs000064400000000000000000000254121046102023000162300ustar 00000000000000use std::mem; use std::pin::Pin; use std::task::{Context, Poll, Waker}; use std::time::Duration; use futures_util::stream::{BoxStream, SelectAll}; use futures_util::{stream, FutureExt, Stream, StreamExt}; use crate::{Delay, PushError, Timeout}; /// Represents a map of [`Stream`]s. /// /// Each stream must finish within the specified time and the map never outgrows its capacity. pub struct StreamMap { make_delay: Box Delay + Send + Sync>, capacity: usize, inner: SelectAll>>>, empty_waker: Option, full_waker: Option, } impl StreamMap where ID: Clone + Unpin, { pub fn new(make_delay: impl Fn() -> Delay + Send + Sync + 'static, capacity: usize) -> Self { Self { make_delay: Box::new(make_delay), capacity, inner: Default::default(), empty_waker: None, full_waker: None, } } } impl StreamMap where ID: Clone + PartialEq + Send + Unpin + 'static, O: Send + 'static, { /// Push a stream into the map. pub fn try_push(&mut self, id: ID, stream: F) -> Result<(), PushError>> where F: Stream + Send + 'static, { if self.inner.len() >= self.capacity { return Err(PushError::BeyondCapacity(stream.boxed())); } if let Some(waker) = self.empty_waker.take() { waker.wake(); } let old = self.remove(id.clone()); self.inner.push(TaggedStream::new( id, TimeoutStream { inner: stream.boxed(), timeout: (self.make_delay)(), }, )); match old { None => Ok(()), Some(old) => Err(PushError::Replaced(old)), } } pub fn remove(&mut self, id: ID) -> Option> { let tagged = self.inner.iter_mut().find(|s| s.key == id)?; let inner = mem::replace(&mut tagged.inner.inner, stream::pending().boxed()); tagged.exhausted = true; // Setting this will emit `None` on the next poll and ensure `SelectAll` cleans up the resources. Some(inner) } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } #[allow(unknown_lints, clippy::needless_pass_by_ref_mut)] // &mut Context is idiomatic. pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { if self.inner.len() < self.capacity { return Poll::Ready(()); } self.full_waker = Some(cx.waker().clone()); Poll::Pending } pub fn poll_next_unpin( &mut self, cx: &mut Context<'_>, ) -> Poll<(ID, Option>)> { match futures_util::ready!(self.inner.poll_next_unpin(cx)) { None => { self.empty_waker = Some(cx.waker().clone()); Poll::Pending } Some((id, Some(Ok(output)))) => Poll::Ready((id, Some(Ok(output)))), Some((id, Some(Err(dur)))) => { self.remove(id.clone()); // Remove stream, otherwise we keep reporting the timeout. Poll::Ready((id, Some(Err(Timeout::new(dur))))) } Some((id, None)) => Poll::Ready((id, None)), } } } struct TimeoutStream { inner: S, timeout: Delay, } impl Stream for TimeoutStream where F: Stream + Unpin, { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if let Poll::Ready(dur) = self.timeout.poll_unpin(cx) { return Poll::Ready(Some(Err(dur))); } self.inner.poll_next_unpin(cx).map(|a| a.map(Ok)) } } struct TaggedStream { key: K, inner: S, exhausted: bool, } impl TaggedStream { fn new(key: K, inner: S) -> Self { Self { key, inner, exhausted: false, } } } impl Stream for TaggedStream where K: Clone + Unpin, S: Stream + Unpin, { type Item = (K, Option); fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.exhausted { return Poll::Ready(None); } match futures_util::ready!(self.inner.poll_next_unpin(cx)) { Some(item) => Poll::Ready(Some((self.key.clone(), Some(item)))), None => { self.exhausted = true; Poll::Ready(Some((self.key.clone(), None))) } } } } #[cfg(all(test, feature = "futures-timer"))] mod tests { use futures::channel::mpsc; use futures_util::stream::{once, pending}; use futures_util::SinkExt; use std::future::{poll_fn, ready, Future}; use std::pin::Pin; use std::time::Instant; use super::*; #[test] fn cannot_push_more_than_capacity_tasks() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_secs(10)), 1); assert!(streams.try_push("ID_1", once(ready(()))).is_ok()); matches!( streams.try_push("ID_2", once(ready(()))), Err(PushError::BeyondCapacity(_)) ); } #[test] fn cannot_push_the_same_id_few_times() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_secs(10)), 5); assert!(streams.try_push("ID", once(ready(()))).is_ok()); matches!( streams.try_push("ID", once(ready(()))), Err(PushError::Replaced(_)) ); } #[tokio::test] async fn streams_timeout() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 1); let _ = streams.try_push("ID", pending::<()>()); futures_timer::Delay::new(Duration::from_millis(150)).await; let (_, result) = poll_fn(|cx| streams.poll_next_unpin(cx)).await; assert!(result.unwrap().is_err()) } #[tokio::test] async fn timed_out_stream_gets_removed() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 1); let _ = streams.try_push("ID", pending::<()>()); futures_timer::Delay::new(Duration::from_millis(150)).await; poll_fn(|cx| streams.poll_next_unpin(cx)).await; let poll = streams.poll_next_unpin(&mut Context::from_waker( futures_util::task::noop_waker_ref(), )); assert!(poll.is_pending()) } #[test] fn removing_stream() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 1); let _ = streams.try_push("ID", stream::once(ready(()))); { let cancelled_stream = streams.remove("ID"); assert!(cancelled_stream.is_some()); } let poll = streams.poll_next_unpin(&mut Context::from_waker( futures_util::task::noop_waker_ref(), )); assert!(poll.is_pending()); assert_eq!( streams.len(), 0, "resources of cancelled streams are cleaned up properly" ); } #[tokio::test] async fn replaced_stream_is_still_registered() { let mut streams = StreamMap::new(|| Delay::futures_timer(Duration::from_millis(100)), 3); let (mut tx1, rx1) = mpsc::channel(5); let (mut tx2, rx2) = mpsc::channel(5); let _ = streams.try_push("ID1", rx1); let _ = streams.try_push("ID2", rx2); let _ = tx2.send(2).await; let _ = tx1.send(1).await; let _ = tx2.send(3).await; let (id, res) = poll_fn(|cx| streams.poll_next_unpin(cx)).await; assert_eq!(id, "ID1"); assert_eq!(res.unwrap().unwrap(), 1); let (id, res) = poll_fn(|cx| streams.poll_next_unpin(cx)).await; assert_eq!(id, "ID2"); assert_eq!(res.unwrap().unwrap(), 2); let (id, res) = poll_fn(|cx| streams.poll_next_unpin(cx)).await; assert_eq!(id, "ID2"); assert_eq!(res.unwrap().unwrap(), 3); let (mut new_tx1, new_rx1) = mpsc::channel(5); let replaced = streams.try_push("ID1", new_rx1); assert!(matches!(replaced.unwrap_err(), PushError::Replaced(_))); let _ = new_tx1.send(4).await; let (id, res) = poll_fn(|cx| streams.poll_next_unpin(cx)).await; assert_eq!(id, "ID1"); assert_eq!(res.unwrap().unwrap(), 4); } // Each stream emits 1 item with delay, `Task` only has a capacity of 1, meaning they must be processed in sequence. // We stop after NUM_STREAMS tasks, meaning the overall execution must at least take DELAY * NUM_STREAMS. #[tokio::test] async fn backpressure() { const DELAY: Duration = Duration::from_millis(100); const NUM_STREAMS: u32 = 10; let start = Instant::now(); Task::new(DELAY, NUM_STREAMS, 1).await; let duration = start.elapsed(); assert!(duration >= DELAY * NUM_STREAMS); } struct Task { item_delay: Duration, num_streams: usize, num_processed: usize, inner: StreamMap, } impl Task { fn new(item_delay: Duration, num_streams: u32, capacity: usize) -> Self { Self { item_delay, num_streams: num_streams as usize, num_processed: 0, inner: StreamMap::new(|| Delay::futures_timer(Duration::from_secs(60)), capacity), } } } impl Future for Task { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); while this.num_processed < this.num_streams { match this.inner.poll_next_unpin(cx) { Poll::Ready((_, Some(result))) => { if result.is_err() { panic!("Timeout is great than item delay") } this.num_processed += 1; continue; } Poll::Ready((_, None)) => { continue; } _ => {} } if let Poll::Ready(()) = this.inner.poll_ready_unpin(cx) { // We push the constant ID to prove that user can use the same ID if the stream was finished let maybe_future = this .inner .try_push(1u8, once(futures_timer::Delay::new(this.item_delay))); assert!(maybe_future.is_ok(), "we polled for readiness"); continue; } return Poll::Pending; } Poll::Ready(()) } } } futures-bounded-0.3.0/src/stream_set.rs000064400000000000000000000034031046102023000162420ustar 00000000000000use futures_util::stream::BoxStream; use futures_util::Stream; use std::task::{ready, Context, Poll}; use crate::{Delay, PushError, StreamMap, Timeout}; /// Represents a set of [Stream]s. /// /// Each stream must finish within the specified time and the list never outgrows its capacity. pub struct StreamSet { id: u32, inner: StreamMap, } impl StreamSet { pub fn new(make_delay: impl Fn() -> Delay + Send + Sync + 'static, capacity: usize) -> Self { Self { id: 0, inner: StreamMap::new(make_delay, capacity), } } } impl StreamSet where O: Send + 'static, { /// Push a stream into the list. /// /// This method adds the given stream to the list. /// If the length of the list is equal to the capacity, this method returns a error that contains the passed stream. /// In that case, the stream is not added to the set. pub fn try_push(&mut self, stream: F) -> Result<(), BoxStream> where F: Stream + Send + 'static, { self.id = self.id.wrapping_add(1); match self.inner.try_push(self.id, stream) { Ok(()) => Ok(()), Err(PushError::BeyondCapacity(w)) => Err(w), Err(PushError::Replaced(_)) => unreachable!("we never reuse IDs"), } } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { self.inner.poll_ready_unpin(cx) } pub fn poll_next_unpin(&mut self, cx: &mut Context<'_>) -> Poll>> { let (_, res) = ready!(self.inner.poll_next_unpin(cx)); Poll::Ready(res) } }