Skip to content

Commit

Permalink
fix(client): detect connection closes as pool tries to use
Browse files Browse the repository at this point in the history
Currently, if the remote closes the connection at the same time that the
pool selects it to use for a new request, the connection may actually
hang. This fix will now more allow the keep-alive read to check the
socket even when the `Conn` think it's busy.

If the connection was closed before the request write happened, returns
back an `Error::Cancel`, letting the user know they could safely retry
it.

Closes #1439
  • Loading branch information
seanmonstar committed Feb 13, 2018
1 parent a9413d7 commit dc619a8
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 90 deletions.
2 changes: 1 addition & 1 deletion src/client/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl<T, U> Drop for Receiver<T, U> {
// - Err: unreachable
while let Ok(Async::Ready(Some((_val, cb)))) = self.inner.poll() {
// maybe in future, we pass the value along with the error?
let _ = cb.send(Err(::Error::new_canceled()));
let _ = cb.send(Err(::Error::new_canceled(None)));
}
}

Expand Down
6 changes: 1 addition & 5 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,7 @@ where C: Connect,
},
Err(_) => {
error!("pooled connection was not ready, this is a hyper bug");
let err = io::Error::new(
io::ErrorKind::BrokenPipe,
"pool selected dead connection",
);
Either::B(future::err(::Error::Io(err)))
Either::B(future::err(::Error::new_canceled(None)))
}
}
});
Expand Down
17 changes: 5 additions & 12 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ pub enum Error {
}

impl Error {
pub(crate) fn new_canceled() -> Error {
pub(crate) fn new_canceled(cause: Option<Error>) -> Error {
Error::Cancel(Canceled {
_inner: (),
cause: cause.map(Box::new),
})
}
}
Expand All @@ -73,10 +73,9 @@ impl Error {
/// as the related connection gets closed by the remote. In that case,
/// when the connection drops, the pending response future will be
/// fulfilled with this error, signaling the `Request` was never started.
#[derive(Debug)]
pub struct Canceled {
// maybe in the future this contains an optional value of
// what was canceled?
_inner: (),
cause: Option<Box<Error>>,
}

impl Canceled {
Expand All @@ -85,13 +84,6 @@ impl Canceled {
}
}

impl fmt::Debug for Canceled {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Canceled")
.finish()
}
}

impl fmt::Display for Canceled {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(self.description())
Expand Down Expand Up @@ -142,6 +134,7 @@ impl StdError for Error {
Io(ref error) => Some(error),
Uri(ref error) => Some(error),
Utf8(ref error) => Some(error),
Cancel(ref e) => e.cause.as_ref().map(|e| &**e as &StdError),
Error::__Nonexhaustive(..) => unreachable!(),
_ => None,
}
Expand Down
82 changes: 58 additions & 24 deletions src/proto/h1/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ where I: AsyncRead + AsyncWrite,
} else {
trace!("poll when on keep-alive");
if !T::should_read_first() {
self.try_empty_read()?;
self.require_empty_read()?;
if self.is_read_closed() {
return Ok(Async::Ready(None));
}
Expand Down Expand Up @@ -281,17 +281,23 @@ where I: AsyncRead + AsyncWrite,
pub fn read_keep_alive(&mut self) -> Result<(), ::Error> {
debug_assert!(!self.can_read_head() && !self.can_read_body());

trace!("Conn::read_keep_alive");
trace!("read_keep_alive; is_mid_message={}", self.is_mid_message());

if T::should_read_first() || !self.state.is_idle() {
if self.is_mid_message() {
self.maybe_park_read();
} else {
self.try_empty_read()?;
self.require_empty_read()?;
}

Ok(())
}

fn is_mid_message(&self) -> bool {
match (&self.state.reading, &self.state.writing) {
(&Reading::Init, &Writing::Init) => false,
_ => true,
}
}

fn maybe_park_read(&mut self) {
if !self.io.is_read_blocked() {
// the Io object is ready to read, which means it will never alert
Expand All @@ -312,40 +318,68 @@ where I: AsyncRead + AsyncWrite,
//
// This should only be called for Clients wanting to enter the idle
// state.
fn try_empty_read(&mut self) -> io::Result<()> {
fn require_empty_read(&mut self) -> io::Result<()> {
assert!(!self.can_read_head() && !self.can_read_body());

if !self.io.read_buf().is_empty() {
debug!("received an unexpected {} bytes", self.io.read_buf().len());
Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected bytes after message ended"))
} else {
match self.io.read_from_io() {
Ok(Async::Ready(0)) => {
trace!("try_empty_read; found EOF on connection: {:?}", self.state);
let must_error = self.should_error_on_eof();
// order is important: must_error needs state BEFORE close_read
self.state.close_read();
if must_error {
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected EOF waiting for response"))
} else {
Ok(())
}
match self.try_io_read()? {
Async::Ready(0) => {
// case handled in try_io_read
Ok(())
},
Ok(Async::Ready(n)) => {
Async::Ready(n) => {
debug!("received {} bytes on an idle connection", n);
Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected bytes after message ended"))
let desc = if self.state.is_idle() {
"unexpected bytes after message ended"
} else {
"unexpected bytes before writing message"
};
Err(io::Error::new(io::ErrorKind::InvalidData, desc))
},
Ok(Async::NotReady) => {
Async::NotReady => {
Ok(())
},
Err(e) => {
self.state.close();
Err(e)
}
}
}
}

fn try_io_read(&mut self) -> Poll<usize, io::Error> {
match self.io.read_from_io() {
Ok(Async::Ready(0)) => {
trace!("try_io_read; found EOF on connection: {:?}", self.state);
let must_error = self.should_error_on_eof();
let ret = if must_error {
let desc = if self.is_mid_message() {
"unexpected EOF waiting for response"
} else {
"unexpected EOF before writing message"
};
Err(io::Error::new(io::ErrorKind::UnexpectedEof, desc))
} else {
Ok(Async::Ready(0))
};

// order is important: must_error needs state BEFORE close_read
self.state.close_read();
ret
},
Ok(Async::Ready(n)) => {
Ok(Async::Ready(n))
},
Ok(Async::NotReady) => {
Ok(Async::NotReady)
},
Err(e) => {
self.state.close();
Err(e)
}
}
}


fn maybe_notify(&mut self) {
// its possible that we returned NotReady from poll() without having
// exhausted the underlying Io. We would have done this when we
Expand Down
22 changes: 15 additions & 7 deletions src/proto/h1/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,9 @@ where
let _ = cb.send(Err(err));
Ok(())
} else if let Ok(Async::Ready(Some((_, cb)))) = self.rx.poll() {
let _ = cb.send(Err(err));
// in this case, the message was never even started, so it's safe to tell
// the user that the request was completely canceled
let _ = cb.send(Err(::Error::new_canceled(Some(err))));
Ok(())
} else {
Err(err)
Expand Down Expand Up @@ -431,13 +433,14 @@ where

#[cfg(test)]
mod tests {
extern crate pretty_env_logger;

use super::*;
use mock::AsyncIo;
use proto::ClientTransaction;

#[test]
fn client_read_response_before_writing_request() {
extern crate pretty_env_logger;
fn client_read_bytes_before_writing_request() {
let _ = pretty_env_logger::try_init();
::futures::lazy(|| {
let io = AsyncIo::new_buf(b"HTTP/1.1 200 OK\r\n\r\n".to_vec(), 100);
Expand All @@ -452,11 +455,16 @@ mod tests {
};
let res_rx = tx.send((req, None::<::Body>)).unwrap();

dispatcher.poll().expect("dispatcher poll 1");
dispatcher.poll().expect("dispatcher poll 2");
let _res = res_rx.wait()
let a1 = dispatcher.poll().expect("error should be sent on channel");
assert!(a1.is_ready(), "dispatcher should be closed");
let err = res_rx.wait()
.expect("callback poll")
.expect("callback response");
.expect_err("callback response");

match err {
::Error::Cancel(_) => (),
other => panic!("expected Cancel(_), got {:?}", other),
}
Ok::<(), ()>(())
}).wait().unwrap();
}
Expand Down
Loading

0 comments on commit dc619a8

Please sign in to comment.