use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::time::{Instant, Sleep};
pub const IDLE_CLOSED: &str = "no bytes moved on this git transfer";
#[derive(Debug, Clone, Default)]
pub struct IdleFlag(Arc<AtomicBool>);
impl IdleFlag {
pub fn timed_out(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
}
pub struct Idle<S> {
inner: S,
timeout: Option<Duration>,
last: Instant,
timer: Option<Pin<Box<Sleep>>>,
flag: IdleFlag,
}
impl<S> Idle<S> {
pub fn new(inner: S, timeout: Duration) -> (Self, IdleFlag) {
Self::maybe(inner, Some(timeout))
}
pub fn maybe(inner: S, timeout: Option<Duration>) -> (Self, IdleFlag) {
let flag = IdleFlag::default();
let now = Instant::now();
(
Idle {
inner,
timeout,
last: now,
timer: timeout.map(|t| Box::pin(tokio::time::sleep_until(now + t))),
flag: flag.clone(),
},
flag,
)
}
fn touch(&mut self) {
self.last = Instant::now();
}
fn error(&self) -> io::Error {
let secs = self.timeout.map(|t| t.as_secs()).unwrap_or_default();
io::Error::new(
io::ErrorKind::TimedOut,
format!("{IDLE_CLOSED} for {secs} s while it waited on the client; it is closed and its transfer slot is free again"),
)
}
fn expired(&mut self, cx: &mut Context<'_>) -> Poll<io::Error> {
let Some(timeout) = self.timeout else {
return Poll::Pending;
};
let last = self.last;
let Some(timer) = self.timer.as_mut() else {
return Poll::Pending;
};
loop {
if timer.as_mut().poll(cx).is_pending() {
return Poll::Pending;
}
let deadline = last + timeout;
if Instant::now() >= deadline {
break;
}
timer.as_mut().reset(deadline);
}
self.flag.0.store(true, Ordering::SeqCst);
Poll::Ready(self.error())
}
}
impl<S> std::fmt::Debug for Idle<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Idle")
.field("timeout", &self.timeout)
.finish_non_exhaustive()
}
}
impl<S: AsyncRead + Unpin> AsyncRead for Idle<S> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let me = self.get_mut();
if me.flag.timed_out() {
return Poll::Ready(Err(me.error()));
}
let before = buf.filled().len();
match Pin::new(&mut me.inner).poll_read(cx, buf) {
Poll::Ready(Ok(())) => {
if buf.filled().len() > before {
me.touch();
}
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => me.expired(cx).map(Err),
}
}
}
impl<S: AsyncWrite + Unpin> AsyncWrite for Idle<S> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let me = self.get_mut();
if me.flag.timed_out() {
return Poll::Ready(Err(me.error()));
}
match Pin::new(&mut me.inner).poll_write(cx, buf) {
Poll::Ready(Ok(n)) => {
if n > 0 {
me.touch();
}
Poll::Ready(Ok(n))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => me.expired(cx).map(Err),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let me = self.get_mut();
match Pin::new(&mut me.inner).poll_flush(cx) {
Poll::Pending => me.expired(cx).map(Err),
ready => ready,
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
const T: Duration = Duration::from_millis(400);
#[tokio::test]
async fn a_read_that_waits_past_the_timeout_fails_by_name() {
let (_client, server) = tokio::io::duplex(64);
let (mut idle, flag) = Idle::new(server, T);
let started = std::time::Instant::now();
let mut buf = [0u8; 8];
let err = tokio::time::timeout(Duration::from_secs(5), idle.read(&mut buf))
.await
.expect("the idle read ends")
.expect_err("an idle read times out");
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(err.to_string().contains(IDLE_CLOSED), "{err}");
assert!(flag.timed_out());
let waited = started.elapsed();
assert!(waited >= T && waited < 4 * T, "{waited:?}");
}
#[tokio::test]
async fn bytes_either_way_keep_the_transfer_alive() {
let (mut client, server) = tokio::io::duplex(64);
let (mut idle, flag) = Idle::new(server, T);
let feeder = tokio::spawn(async move {
for _ in 0..4 {
tokio::time::sleep(T / 2).await;
client.write_all(b"x").await.unwrap();
}
client
});
let mut buf = [0u8; 1];
for _ in 0..4 {
idle.read_exact(&mut buf)
.await
.expect("a live client is not idle");
}
let mut client = feeder.await.unwrap();
tokio::time::sleep(T * 3 / 4).await;
idle.write_all(b"y").await.expect("the server writes");
let mut got = [0u8; 1];
client.read_exact(&mut got).await.unwrap();
let read = tokio::time::timeout(T / 2, idle.read(&mut buf)).await;
assert!(
read.is_err(),
"the server's own write was activity: {read:?}"
);
assert!(!flag.timed_out());
}
}