//! An [`AsyncWrite`] rate limiter. use std::future::Future; use std::num::NonZero; use std::pin::Pin; use std::task::{Context, Poll}; use web_time_compat::{Duration, Instant}; use futures::AsyncWrite; use futures::io::Error; use sync_wrapper::SyncFuture; use tor_rtcompat::SleepProvider; use tor_basic_utils::token_bucket::{NeverEnoughTokensError, TokenBucket, TokenBucketConfig}; /// A rate-limited async [writer](AsyncWrite). /// /// This can be used as a wrapper around an existing [`AsyncWrite`] writer. #[derive(educe::Educe)] #[educe(Debug)] #[pin_project::pin_project] pub struct RateLimitedWriter { /// The token bucket. bucket: TokenBucket, /// The sleep provider, for getting the current time and creating new sleep futures. /// /// While we use [`Instant`] for the time, we should always get the time from this /// [`SleepProvider`]. /// For example, use [`SleepProvider::now()`], /// not [`Instant::now()`](std::time::Instant::now) or /// [`InstantExt::get`](web_time_compat::InstantExt::get). #[educe(Debug(ignore))] sleep_provider: P, /// See [`RateLimitedWriterConfig::wake_when_bytes_available`]. wake_when_bytes_available: NonZero, /// The inner writer. #[educe(Debug(ignore))] #[pin] inner: W, /// We need to store the sleep future if [`AsyncWrite::poll_write()`] blocks. #[educe(Debug(ignore))] #[pin] sleep_fut: Option>, } impl RateLimitedWriter where W: AsyncWrite, P: SleepProvider, { /// Create a new [`RateLimitedWriter`]. // We take the rate and bucket max directly rather than a `TokenBucket` to ensure that the token // bucket only ever uses times from `sleep_provider`. pub fn new(writer: W, config: &RateLimitedWriterConfig, sleep_provider: P) -> Self { let bucket_config = TokenBucketConfig { rate: config.rate, bucket_max: config.burst, }; Self::from_token_bucket( writer, TokenBucket::new(&bucket_config, sleep_provider.now()), config.wake_when_bytes_available, sleep_provider, ) } /// Create a new [`RateLimitedWriter`] from a [`TokenBucket`]. /// /// The token bucket must have only been used with times created by `sleep_provider`. #[cfg_attr(test, visibility::make(pub(super)))] fn from_token_bucket( writer: W, bucket: TokenBucket, wake_when_bytes_available: NonZero, sleep_provider: P, ) -> Self { Self { bucket, sleep_provider, wake_when_bytes_available, inner: writer, sleep_fut: None, } } /// Access the inner [`AsyncWrite`] writer. pub fn inner(&self) -> &W { &self.inner } /// Adjust the refill rate and burst. /// /// A rate and/or burst of 0 is allowed. pub fn adjust(self: &mut Pin<&mut Self>, now: Instant, config: &RateLimitedWriterConfig) { let self_ = self.as_mut().project(); // destructuring allows us to make sure we aren't forgetting to handle any fields let RateLimitedWriterConfig { rate, burst, wake_when_bytes_available, } = *config; let bucket_config = TokenBucketConfig { rate, bucket_max: burst, }; self_.bucket.adjust(now, &bucket_config); *self_.wake_when_bytes_available = wake_when_bytes_available; } /// The sleep provider. /// /// We don't want this to be generally accessible, only to other token bucket-related modules /// like [`DynamicRateLimitedWriter`](super::dynamic_writer::DynamicRateLimitedWriter). pub(super) fn sleep_provider(&self) -> &P { &self.sleep_provider } /// Configure this writer to sleep for `duration`. /// /// A `duration` of `None` is interpreted as "forever". /// /// It's considered a bug if asked to sleep for `Duration::ZERO` time. fn register_sleep( sleep_fut: &mut Pin<&mut Option>>, sleep_provider: &mut P, cx: &mut Context<'_>, duration: Option, ) -> Poll<()> { match duration { None => { sleep_fut.as_mut().set(None); Poll::Pending } Some(duration) => { debug_assert_ne!(duration, Duration::ZERO, "asked to sleep for 0 time"); sleep_fut .as_mut() .set(Some(SyncFuture::new(sleep_provider.sleep(duration)))); sleep_fut .as_mut() .as_pin_mut() .expect("but we just set it to `Some`?!") .poll(cx) } } } } impl AsyncWrite for RateLimitedWriter where W: AsyncWrite, P: SleepProvider, { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, mut buf: &[u8], ) -> Poll> { let mut self_ = self.as_mut().project(); // this should be optimized to a no-op on at least x86-64 fn to_u64(x: usize) -> u64 { x.try_into().expect("failed usize to u64 conversion") } // for an empty buffer, just defer to the inner writer's impl if buf.is_empty() { return self_.inner.poll_write(cx, buf); } let now = self_.sleep_provider.now(); // refill the bucket and attempt to claim all of the bytes self_.bucket.refill(now); let claim = self_.bucket.claim(to_u64(buf.len())); let mut claim = match claim { // claim was successful Ok(x) => x, // not enough tokens, so let's use a smaller buffer Err(e) => { let available = e.available_tokens(); // need to drop the old claim so that we can access the token bucket again drop(claim); // if no tokens in bucket, we must sleep if available == 0 { // number of tokens we'll wait for let wake_at_tokens = to_u64(buf.len()); // If the user wants to write X tokens, we don't necessarily want to sleep until // we have room for X tokens. We also don't want to wake every time that a // single byte can be written. We allow the user to configure this threshold // with `RateLimitedWriterConfig::wake_when_bytes_available`. let wake_at_tokens = std::cmp::min(wake_at_tokens, self_.wake_when_bytes_available.get()); // max number of tokens the bucket can hold let bucket_max = self_.bucket.max(); // how long to sleep for; `None` indicates to sleep forever let sleep_for = if bucket_max == 0 { // bucket can't hold any tokens, so sleep forever None } else { // if the bucket has a max of X tokens, we should never try to wait for >X // tokens let wake_at_tokens = std::cmp::min(wake_at_tokens, bucket_max); // if we asked for 0 tokens, we'd get a time of ~now, which is not what we // want debug_assert!(wake_at_tokens > 0); let wake_at = self_.bucket.tokens_available_at(wake_at_tokens); let sleep_for = wake_at.map(|x| x.saturating_duration_since(now)); match sleep_for { Ok(x) => Some(x), Err(NeverEnoughTokensError::ExceedsMaxTokens) => { panic!( "exceeds max tokens, but we took the max into account above" ); } // we aren't refilling, so sleep forever Err(NeverEnoughTokensError::ZeroRate) => None, // too far in the future to be represented, so sleep forever Err(NeverEnoughTokensError::InstantNotRepresentable) => None, } }; // configure the sleep future and poll it to register let poll = Self::register_sleep( &mut self_.sleep_fut, self_.sleep_provider, cx, sleep_for, ); return match poll { // wait for the sleep to finish Poll::Pending => Poll::Pending, // The sleep is already ready?! A recursive call here isn't great, but // there's not much else we can do here. Hopefully this second `poll_write` // will succeed since we should now have enough tokens. Poll::Ready(()) => self.poll_write(cx, buf), }; } /// Convert a `u64` to `usize`, saturating if size of `usize` is smaller than `u64`. // This is a separate function to ensure we don't accidentally try to convert a // signed integer into a `usize`, in which case `unwrap_or(MAX)` wouldn't make // sense. fn to_usize_saturating(x: u64) -> usize { x.try_into().unwrap_or(usize::MAX) } // There are tokens, so try to write as many as are available. let available_usize = to_usize_saturating(available); buf = &buf[0..available_usize]; self_.bucket.claim(to_u64(buf.len())).unwrap_or_else(|_| { panic!( "bucket has {available} tokens available, but can't claim {}?", buf.len(), ) }) } }; let rv = self_.inner.poll_write(cx, buf); match rv { // no bytes were written, so discard the claim Poll::Pending | Poll::Ready(Err(_)) => claim.discard(), // `x` bytes were written, so only commit those tokens Poll::Ready(Ok(x)) => { if x <= buf.len() { claim .reduce(to_u64(x)) .expect("can't commit fewer tokens?!"); claim.commit(); } else { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { panic!( "Writer is claiming it wrote more bytes {x} than we gave it {}", buf.len(), ); } else { // the best we can do is to just claim the original amount claim.commit(); } } } } }; rv } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().inner.poll_flush(cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // some implementers of `AsyncWrite` (like `Vec`) don't do anything other than flush when // closed and will continue to accept bytes even after being closed, so we must continue to // apply rate limiting even after being closed self.project().inner.poll_close(cx) } } /// A module to make it easier to implement tokio traits without putting `cfg()` conditionals /// everywhere. #[cfg(feature = "tokio")] mod tokio_impl { use super::*; use tokio::io::AsyncWrite as TokioAsyncWrite; use tokio_util::compat::FuturesAsyncWriteCompatExt; use std::io::Result as IoResult; impl TokioAsyncWrite for RateLimitedWriter where W: AsyncWrite, P: SleepProvider, { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { TokioAsyncWrite::poll_write(Pin::new(&mut self.compat_write()), cx, buf) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { TokioAsyncWrite::poll_flush(Pin::new(&mut self.compat_write()), cx) } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { TokioAsyncWrite::poll_shutdown(Pin::new(&mut self.compat_write()), cx) } } } /// The refill rate and burst for a [`RateLimitedWriter`]. #[derive(Clone, Debug)] #[allow(clippy::exhaustive_structs)] pub struct RateLimitedWriterConfig { /// The refill rate in bytes/second. pub rate: u64, /// The "burst" in bytes. pub burst: u64, /// When polled, block until at most this many bytes are available. /// /// Or in other words, wake when we can write this many bytes, even if the provided buffer is /// larger. /// /// For example if a user attempts to write a large buffer, we usually don't want to block until /// the entire buffer can be written. We'd prefer several partial writes to a single large /// write. So instead of blocking until the entire buffer can be written, we only block until /// at most this many bytes are available. pub wake_when_bytes_available: NonZero, } #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] use super::*; use futures::{AsyncWriteExt, FutureExt}; use tor_rtcompat::SpawnExt; #[test] fn writer() { tor_rtmock::MockRuntime::test_with_various(|rt| async move { let start = rt.now(); // increases 10 tokens/second (one every 100 ms) let config = TokenBucketConfig { rate: 10, bucket_max: 100, }; let mut tb = TokenBucket::new(&config, start); // drain the bucket tb.drain(100).unwrap(); let wake_when_bytes_available = NonZero::new(15).unwrap(); let mut writer = Vec::new(); let mut writer = RateLimitedWriter::from_token_bucket( &mut writer, tb, wake_when_bytes_available, rt.clone(), ); // drive time forward from 0 to 20_000 ms in 50 ms intervals let rt_clone = rt.clone(); rt.spawn(async move { for _ in 0..400 { rt_clone.progress_until_stalled().await; rt_clone.advance_by(Duration::from_millis(50)).await; } }) .unwrap(); // try writing 60 bytes, which sleeps until we can write at least 15 of them assert_eq!(15, writer.write(&[0; 60]).await.unwrap()); assert_eq!(1500, rt.now().duration_since(start).as_millis()); // wait 2 seconds rt.sleep(Duration::from_millis(2000)).await; // ensure that we can write immediately, and that we can write // 2000 ms / (100 ms/token) = 20 bytes assert_eq!( Some(20), writer.write(&[0; 60]).now_or_never().map(Result::unwrap), ); }); } /// Test that writing to a token bucket which has a rate and/or max of 0 works as expected. #[test] fn rate_burst_zero() { let configs = [ // non-zero rate, zero max TokenBucketConfig { rate: 10, bucket_max: 0, }, // zero rate, non-zero max TokenBucketConfig { rate: 0, bucket_max: 10, }, // zero rate, zero max TokenBucketConfig { rate: 0, bucket_max: 0, }, ]; for config in configs { tor_rtmock::MockRuntime::test_with_various(|rt| { let config = config.clone(); async move { // an empty token bucket let mut tb = TokenBucket::new(&config, rt.now()); tb.drain(tb.max()).unwrap(); assert!(tb.is_empty()); let wake_when_bytes_available = NonZero::new(2).unwrap(); let mut writer = Vec::new(); let mut writer = RateLimitedWriter::from_token_bucket( &mut writer, tb, wake_when_bytes_available, rt.clone(), ); // drive time forward from 0 to 10_000 ms in 100 ms intervals let rt_clone = rt.clone(); rt.spawn(async move { for _ in 0..100 { rt_clone.progress_until_stalled().await; rt_clone.advance_by(Duration::from_millis(100)).await; } }) .unwrap(); // ensure that a write returns `Pending` assert_eq!( None, writer.write(&[0; 60]).now_or_never().map(Result::unwrap), ); // wait 5 seconds rt.sleep(Duration::from_millis(5000)).await; // ensure that a write still returns `Pending` assert_eq!( None, writer.write(&[0; 60]).now_or_never().map(Result::unwrap), ); } }); } } }