use std::cell::Cell;
use std::fmt;
use std::io::{self, Read, Write};
use std::net::{Shutdown, SocketAddr};
use std::time::{Duration, Instant};
use super::stream::NetStream;
#[derive(Debug)]
struct DeadlineExceeded;
impl fmt::Display for DeadlineExceeded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("operation timed out: maximum transfer time (--max-time) reached")
}
}
impl std::error::Error for DeadlineExceeded {}
pub(crate) fn deadline_exceeded() -> io::Error {
io::Error::new(io::ErrorKind::TimedOut, DeadlineExceeded)
}
pub(crate) fn is_deadline_exceeded(e: &io::Error) -> bool {
e.get_ref()
.is_some_and(|inner| inner.is::<DeadlineExceeded>())
}
fn remaining(deadline: Instant) -> io::Result<Duration> {
deadline
.checked_duration_since(Instant::now())
.filter(|d| !d.is_zero())
.ok_or_else(deadline_exceeded)
}
pub(crate) fn op_timeout(
deadline: Option<Instant>,
idle: Option<Duration>,
) -> io::Result<Option<Duration>> {
let Some(deadline) = deadline else {
return Ok(idle);
};
let left = remaining(deadline)?;
Ok(Some(idle.map_or(left, |i| i.min(left))))
}
pub(crate) struct DeadlineStream {
inner: Box<dyn NetStream>,
deadline: Instant,
read_idle: Cell<Option<Duration>>,
write_idle: Cell<Option<Duration>>,
}
impl DeadlineStream {
pub(crate) fn wrap(inner: Box<dyn NetStream>, deadline: Option<Instant>) -> Box<dyn NetStream> {
match deadline {
None => inner,
Some(deadline) => Box::new(DeadlineStream {
inner,
deadline,
read_idle: Cell::new(None),
write_idle: Cell::new(None),
}),
}
}
fn budget(&self, idle: Option<Duration>) -> io::Result<(Duration, bool)> {
let left = remaining(self.deadline)?;
Ok(match idle {
Some(i) if i < left => (i, false),
_ => (left, true),
})
}
fn map_err(&self, e: io::Error, by_deadline: bool) -> io::Error {
let timed_out = matches!(
e.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
);
if timed_out && (by_deadline || Instant::now() >= self.deadline) {
deadline_exceeded()
} else {
e
}
}
}
impl Read for DeadlineStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (t, by_deadline) = self.budget(self.read_idle.get())?;
self.inner.set_read_timeout(Some(t))?;
self.inner
.read(buf)
.map_err(|e| self.map_err(e, by_deadline))
}
}
impl Write for DeadlineStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let (t, by_deadline) = self.budget(self.write_idle.get())?;
self.inner.set_write_timeout(Some(t))?;
self.inner
.write(buf)
.map_err(|e| self.map_err(e, by_deadline))
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl NetStream for DeadlineStream {
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
self.inner.set_read_timeout(dur)?;
self.read_idle.set(dur);
Ok(())
}
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
self.inner.set_write_timeout(dur)?;
self.write_idle.set(dur);
Ok(())
}
fn peer_addr(&self) -> io::Result<SocketAddr> {
self.inner.peer_addr()
}
fn local_addr(&self) -> io::Result<SocketAddr> {
self.inner.local_addr()
}
fn shutdown(&self, how: Shutdown) -> io::Result<()> {
self.inner.shutdown(how)
}
fn try_clone_box(&self) -> io::Result<Box<dyn NetStream>> {
Ok(Box::new(DeadlineStream {
inner: self.inner.try_clone_box()?,
deadline: self.deadline,
read_idle: Cell::new(self.read_idle.get()),
write_idle: Cell::new(self.write_idle.get()),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{TcpListener, TcpStream};
fn trickle_server() -> u16 {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
std::thread::spawn(move || {
let (mut s, _) = l.accept().unwrap();
for _ in 0..200 {
if s.write_all(b"x").is_err() {
return;
}
std::thread::sleep(Duration::from_millis(100));
}
});
port
}
#[test]
fn trickling_peer_is_cut_off_at_the_deadline() {
let port = trickle_server();
let start = Instant::now();
let tcp = TcpStream::connect(("127.0.0.1", port)).unwrap();
let mut s = DeadlineStream::wrap(Box::new(tcp), Some(start + Duration::from_millis(500)));
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut got = Vec::new();
let err = s.read_to_end(&mut got).unwrap_err();
let took = start.elapsed();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(is_deadline_exceeded(&err), "{err}");
assert!(!got.is_empty(), "the trickle was being received");
assert!(
took >= Duration::from_millis(450) && took < Duration::from_millis(1500),
"took {took:?}"
);
assert!(is_deadline_exceeded(&s.read(&mut [0u8; 1]).unwrap_err()));
assert!(is_deadline_exceeded(&s.write(b"y").unwrap_err()));
}
#[test]
fn deadline_bounds_a_blocking_read_with_no_idle_timeout() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let h = std::thread::spawn(move || {
let (_s, _) = l.accept().unwrap();
std::thread::sleep(Duration::from_millis(1500));
});
let start = Instant::now();
let tcp = TcpStream::connect(("127.0.0.1", port)).unwrap();
let mut s = DeadlineStream::wrap(Box::new(tcp), Some(start + Duration::from_millis(300)));
s.set_read_timeout(None).unwrap();
let err = s.read(&mut [0u8; 8]).unwrap_err();
assert!(is_deadline_exceeded(&err), "{err}");
assert!(start.elapsed() < Duration::from_millis(1200));
drop(s);
h.join().unwrap();
}
#[test]
fn idle_timeout_shorter_than_deadline_is_an_ordinary_timeout() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let h = std::thread::spawn(move || {
let (_s, _) = l.accept().unwrap();
std::thread::sleep(Duration::from_millis(600));
});
let tcp = TcpStream::connect(("127.0.0.1", port)).unwrap();
let s = DeadlineStream::wrap(
Box::new(tcp),
Some(Instant::now() + Duration::from_secs(10)),
);
s.set_read_timeout(Some(Duration::from_millis(100)))
.unwrap();
let mut c = s.try_clone_box().unwrap();
let err = c.read(&mut [0u8; 8]).unwrap_err();
assert!(matches!(
err.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
));
assert!(!is_deadline_exceeded(&err));
h.join().unwrap();
}
#[test]
fn op_timeout_is_min_of_idle_and_remaining() {
assert_eq!(op_timeout(None, None).unwrap(), None);
let idle = Some(Duration::from_secs(1));
assert_eq!(op_timeout(None, idle).unwrap(), idle);
let far = Instant::now() + Duration::from_secs(100);
assert_eq!(op_timeout(Some(far), idle).unwrap(), idle);
let t = op_timeout(Some(far), None).unwrap().unwrap();
assert!(t > Duration::from_secs(90));
let near = Instant::now() + Duration::from_millis(200);
assert!(op_timeout(Some(near), idle).unwrap().unwrap() <= Duration::from_millis(200));
let past = Instant::now() - Duration::from_millis(1);
let err = op_timeout(Some(past), idle).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(is_deadline_exceeded(&err));
}
#[test]
fn tls_handshake_is_bounded_by_the_deadline() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
std::thread::spawn(move || {
let (_s, _) = l.accept().unwrap();
std::thread::sleep(Duration::from_secs(3));
});
let start = Instant::now();
let err = crate::Client::new()
.max_time(Duration::from_millis(500))
.transfer(&format!("gophers://127.0.0.1:{port}/"))
.unwrap_err();
match &err {
crate::Error::Io(e) => assert!(is_deadline_exceeded(e), "{e}"),
other => panic!("expected the deadline error, got {other:?}"),
}
assert!(start.elapsed() < Duration::from_millis(1500));
}
}