use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use std::task::ready;
use futures::Stream;
use futures::stream::FuturesUnordered;
use pin_project_lite::pin_project;
use tokio::sync::OwnedSemaphorePermit;
use tokio::sync::Semaphore;
use tokio::sync::TryAcquireError;
use vortex_error::VortexExpect;
pin_project! {
struct SizedFut<Fut> {
#[pin]
inner: Fut,
_permits: OwnedSemaphorePermit,
}
}
impl<Fut: Future> Future for SizedFut<Fut> {
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let result = ready!(this.inner.poll(cx));
Poll::Ready(result)
}
}
pin_project! {
pub struct SizeLimitedStream<Fut> {
#[pin]
inflight: FuturesUnordered<SizedFut<Fut>>,
bytes_available: Arc<Semaphore>,
}
}
impl<Fut> SizeLimitedStream<Fut> {
pub fn new(max_bytes: usize) -> Self {
Self {
inflight: FuturesUnordered::new(),
bytes_available: Arc::new(Semaphore::new(max_bytes)),
}
}
pub fn bytes_available(&self) -> usize {
self.bytes_available.available_permits()
}
}
impl<Fut> SizeLimitedStream<Fut>
where
Fut: Future,
{
pub async fn push(&self, fut: Fut, bytes: usize) {
let permits = Arc::clone(&self.bytes_available)
.acquire_many_owned(bytes.try_into().vortex_expect("bytes must fit in u32"))
.await
.unwrap_or_else(|_| unreachable!("pushing to closed semaphore"));
let sized_fut = SizedFut {
inner: fut,
_permits: permits,
};
self.inflight.push(sized_fut);
}
pub fn try_push(&self, fut: Fut, bytes: usize) -> Result<(), Fut> {
match Arc::clone(&self.bytes_available)
.try_acquire_many_owned(bytes.try_into().vortex_expect("bytes must fit in u32"))
{
Ok(permits) => {
let sized_fut = SizedFut {
inner: fut,
_permits: permits,
};
self.inflight.push(sized_fut);
Ok(())
}
Err(acquire_err) => match acquire_err {
TryAcquireError::Closed => {
unreachable!("try_pushing to closed semaphore");
}
TryAcquireError::NoPermits => Err(fut),
},
}
}
}
impl<Fut> Stream for SizeLimitedStream<Fut>
where
Fut: Future,
{
type Item = Fut::Output;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.project();
match ready!(this.inflight.poll_next(cx)) {
None => Poll::Ready(None),
Some(result) => {
Poll::Ready(Some(result))
}
}
}
}
#[cfg(test)]
mod tests {
use std::future;
use std::io;
use std::sync::Arc;
use bytes::Bytes;
use futures::FutureExt;
use futures::StreamExt;
use futures::future::BoxFuture;
use crate::limit::SizeLimitedStream;
async fn make_future(len: usize) -> Bytes {
"a".as_bytes().iter().copied().cycle().take(len).collect()
}
#[tokio::test]
async fn test_size_limit() {
let mut size_limited = SizeLimitedStream::new(10);
size_limited.push(make_future(5), 5).await;
size_limited.push(make_future(5), 5).await;
assert!(size_limited.try_push(make_future(1), 1).is_err());
assert!(size_limited.next().await.is_some());
assert!(size_limited.try_push(make_future(1), 1).is_ok());
}
#[tokio::test]
async fn test_does_not_leak_permits() {
let bad_fut: BoxFuture<'static, io::Result<Bytes>> =
future::ready(Err(io::Error::other("badness"))).boxed();
let good_fut: BoxFuture<'static, io::Result<Bytes>> =
future::ready(Ok(Bytes::from("aaaaa"))).boxed();
let mut size_limited = SizeLimitedStream::new(10);
size_limited.push(bad_fut, 10).await;
let good_fut = size_limited
.try_push(good_fut, 5)
.expect_err("try_push should fail");
let next = size_limited.next().await.unwrap();
assert!(next.is_err());
assert_eq!(size_limited.bytes_available(), 10);
assert!(size_limited.try_push(good_fut, 5).is_ok());
}
#[tokio::test]
async fn test_size_limited_stream_zero_capacity() {
let stream = SizeLimitedStream::new(0);
let result = stream.try_push(async { vec![1u8] }, 1);
assert!(result.is_err());
}
#[tokio::test]
async fn test_size_limited_stream_dropped_future_releases_permits() {
use futures::future::BoxFuture;
let stream = SizeLimitedStream::<BoxFuture<'static, Vec<u8>>>::new(10);
stream
.push(
Box::pin(async {
futures::future::pending::<Vec<u8>>().await
}),
5,
)
.await;
stream.push(Box::pin(async { vec![1u8; 3] }), 3).await;
assert_eq!(stream.bytes_available(), 2);
let permits = Arc::clone(&stream.bytes_available);
drop(stream);
assert_eq!(permits.available_permits(), 10);
}
#[tokio::test]
async fn test_size_limited_stream_exact_capacity() {
use futures::future::BoxFuture;
let mut stream = SizeLimitedStream::<BoxFuture<'static, Vec<u8>>>::new(10);
stream.push(Box::pin(async { vec![0u8; 10] }), 10).await;
let result = stream.try_push(Box::pin(async { vec![1u8] }), 1);
assert!(result.is_err());
stream
.next()
.await
.expect("The 10 byte vector ought to be in there!");
assert_eq!(stream.bytes_available(), 10);
let result = stream.try_push(Box::pin(async { vec![1u8; 5] }), 5);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_size_limited_stream_multiple_small_pushes() {
let mut stream = SizeLimitedStream::new(100);
for i in 0..10 {
#[expect(clippy::cast_possible_truncation)]
stream.push(async move { vec![i as u8; 5] }, 5).await;
}
assert_eq!(stream.bytes_available(), 50);
let mut count = 0;
while stream.next().await.is_some() {
count += 1;
if count == 10 {
break;
}
}
assert_eq!(count, 10);
assert_eq!(stream.bytes_available(), 100);
}
#[cfg(target_pointer_width = "64")]
#[test]
#[should_panic(expected = "bytes must fit in u32")]
fn test_size_overflow_protection() {
let stream = SizeLimitedStream::new(100);
let _result = stream.try_push(async {}, (u32::MAX as usize) + 1);
}
}