use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use std::task::{Context, Poll};
use std::time::Duration;
use futures::task::AtomicWaker;
use tokio::time::{Instant, Sleep};
const NANOS_PER_SEC: u128 = 1_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LeaseWindow {
min: Duration,
max: Duration,
}
impl LeaseWindow {
pub const DEFAULT: Self = Self {
min: Duration::from_mins(15),
max: Duration::from_mins(30),
};
#[must_use]
pub const fn new(min: Duration, max: Duration) -> Option<Self> {
if min.is_zero()
|| min.as_nanos() > max.as_nanos()
|| max.as_nanos().saturating_sub(min.as_nanos()) > u64::MAX as u128
{
None
} else {
Some(Self { min, max })
}
}
#[must_use]
pub const fn min(self) -> Duration {
self.min
}
#[must_use]
pub const fn max(self) -> Duration {
self.max
}
fn at(self, fraction: u64) -> Duration {
let span = self.max.saturating_sub(self.min).as_nanos();
let offset = u128::from(fraction).saturating_mul(span.saturating_add(1)) >> 64;
let secs = u64::try_from(offset / NANOS_PER_SEC).unwrap_or(u64::MAX);
let nanos = u32::try_from(offset % NANOS_PER_SEC).unwrap_or(0);
self.min.saturating_add(Duration::new(secs, nanos))
}
fn draw(self) -> Duration {
use std::hash::BuildHasher as _;
self.at(std::hash::RandomState::new().hash_one(0_u8))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListenLease {
Unbounded,
Renew(LeaseWindow),
}
impl Default for ListenLease {
fn default() -> Self {
Self::Renew(LeaseWindow::DEFAULT)
}
}
#[derive(Debug)]
pub struct ListenLeaseSlot {
window: LeaseWindow,
deadline: OnceLock<Instant>,
waker: AtomicWaker,
}
impl ListenLeaseSlot {
const fn new(window: LeaseWindow) -> Self {
Self {
window,
deadline: OnceLock::new(),
waker: AtomicWaker::new(),
}
}
pub fn start(&self) {
if self
.deadline
.set(super::saturating_deadline(
Instant::now(),
self.window.draw(),
))
.is_ok()
{
self.waker.wake();
}
}
}
struct LeasedBody {
inner: axum::body::Body,
slot: Arc<ListenLeaseSlot>,
expiry: Option<Pin<Box<Sleep>>>,
}
impl http_body::Body for LeasedBody {
type Data = axum::body::Bytes;
type Error = axum::Error;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
use std::future::Future as _;
let this = self.get_mut();
if this.expiry.is_none() {
this.slot.waker.register(cx.waker());
if let Some(deadline) = this.slot.deadline.get() {
this.expiry = Some(Box::pin(tokio::time::sleep_until(*deadline)));
}
}
if let Some(expiry) = this.expiry.as_mut()
&& expiry.as_mut().poll(cx).is_ready()
{
return Poll::Ready(None);
}
Pin::new(&mut this.inner).poll_frame(cx)
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> http_body::SizeHint {
self.inner.size_hint()
}
}
fn is_event_stream(response: &axum::response::Response) -> bool {
response
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("text/event-stream"))
}
pub(super) async fn attach_listen_lease(
axum::extract::State(lease): axum::extract::State<ListenLease>,
mut request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let ListenLease::Renew(window) = lease else {
return next.run(request).await;
};
if request.method() != axum::http::Method::POST {
return next.run(request).await;
}
let slot = Arc::new(ListenLeaseSlot::new(window));
request.extensions_mut().insert(Arc::clone(&slot));
let response = next.run(request).await;
if !is_event_stream(&response) {
return response;
}
let (parts, body) = response.into_parts();
let body = axum::body::Body::new(LeasedBody {
inner: body,
slot,
expiry: None,
});
axum::response::Response::from_parts(parts, body)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn window(min: u64, max: u64) -> LeaseWindow {
LeaseWindow::new(Duration::from_secs(min), Duration::from_secs(max)).unwrap()
}
#[test]
fn test_window_rejects_zero_and_inverted_bounds() {
assert!(LeaseWindow::new(Duration::ZERO, Duration::from_secs(1)).is_none());
assert!(LeaseWindow::new(Duration::from_secs(2), Duration::from_secs(1)).is_none());
assert!(LeaseWindow::new(Duration::from_secs(1), Duration::from_secs(1)).is_some());
}
#[test]
fn test_window_rejects_a_span_beyond_u64_nanos() {
let min = Duration::from_secs(1);
let widest_max = min.saturating_add(Duration::from_nanos(u64::MAX));
let too_long = widest_max.saturating_add(Duration::from_nanos(1));
assert!(LeaseWindow::new(min, too_long).is_none());
let widest = LeaseWindow::new(min, widest_max).unwrap();
assert_eq!(widest.at(u64::MAX), widest.max());
assert_eq!(widest.at(0), widest.min());
}
#[test]
fn test_window_at_hits_both_bounds() {
let w = window(10, 20);
assert_eq!(w.at(0), Duration::from_secs(10));
assert_eq!(w.at(u64::MAX), Duration::from_secs(20));
}
#[test]
fn test_window_degenerate_is_constant() {
let w = window(5, 5);
assert_eq!(w.at(0), Duration::from_secs(5));
assert_eq!(w.at(u64::MAX / 2), Duration::from_secs(5));
}
#[test]
fn test_default_window_is_fifteen_to_thirty_minutes() {
assert_eq!(LeaseWindow::DEFAULT.min(), Duration::from_mins(15));
assert_eq!(LeaseWindow::DEFAULT.max(), Duration::from_mins(30));
}
#[test]
fn test_draw_stays_within_window_and_varies() {
let w = LeaseWindow::DEFAULT;
let draws: Vec<_> = (0..64).map(|_| w.draw()).collect();
assert!(draws.iter().all(|d| (w.min()..=w.max()).contains(d)));
assert!(
draws.iter().any(|d| *d != draws[0]),
"draws must be jittered"
);
}
proptest! {
#[test]
fn prop_at_is_within_window_and_monotonic(
min in 1_u64..100_000,
extra in 0_u64..100_000,
a: u64,
b: u64,
) {
let w = window(min, min + extra);
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
prop_assert!((w.min()..=w.max()).contains(&w.at(lo)));
prop_assert!(w.at(lo) <= w.at(hi));
}
}
async fn next_data(body: &mut LeasedBody) -> Option<Vec<u8>> {
let frame =
std::future::poll_fn(|cx| http_body::Body::poll_frame(Pin::new(&mut *body), cx))
.await?
.unwrap();
Some(frame.into_data().unwrap().to_vec())
}
fn leased(
window: LeaseWindow,
) -> (
tokio::sync::mpsc::Sender<Result<axum::body::Bytes, std::io::Error>>,
Arc<ListenLeaseSlot>,
LeasedBody,
) {
let (tx, mut rx) = tokio::sync::mpsc::channel(4);
let slot = Arc::new(ListenLeaseSlot::new(window));
let body = LeasedBody {
inner: axum::body::Body::from_stream(futures::stream::poll_fn(move |cx| {
rx.poll_recv(cx)
})),
slot: Arc::clone(&slot),
expiry: None,
};
(tx, slot, body)
}
#[tokio::test(start_paused = true)]
async fn test_unstarted_lease_never_ends_the_body() {
let (tx, _slot, mut body) = leased(window(10, 10));
tx.send(Ok(axum::body::Bytes::from_static(b"a")))
.await
.unwrap();
assert_eq!(next_data(&mut body).await.unwrap(), b"a");
tokio::time::sleep(Duration::from_hours(1)).await;
tx.send(Ok(axum::body::Bytes::from_static(b"b")))
.await
.unwrap();
assert_eq!(next_data(&mut body).await.unwrap(), b"b");
}
#[tokio::test(start_paused = true)]
async fn test_started_lease_ends_a_parked_body_at_its_deadline() {
let (_tx, slot, mut body) = leased(window(10, 10));
let begun = Instant::now();
let next = tokio::spawn(async move { next_data(&mut body).await });
tokio::time::sleep(Duration::from_secs(1)).await;
slot.start();
assert!(next.await.unwrap().is_none());
assert_eq!(begun.elapsed(), Duration::from_secs(11));
}
#[tokio::test(start_paused = true)]
async fn test_started_lease_ends_a_chatty_body() {
let (tx, slot, mut body) = leased(window(10, 10));
slot.start();
tokio::time::sleep(Duration::from_secs(11)).await;
tx.send(Ok(axum::body::Bytes::from_static(b"late")))
.await
.unwrap();
assert!(next_data(&mut body).await.is_none());
}
}