feat(client): Add connect timeout to HttpConnector (#1972)
This takes the same strategy as golang, where the timeout value is divided equally between the candidate socket addresses. If happy eyeballs is enabled, the division takes place "below" the IPv4/IPv6 partitioning.
This commit is contained in:
committed by
Sean McArthur
parent
536b1e184e
commit
4179297ac9
@@ -235,6 +235,10 @@ impl IpAddrs {
|
|||||||
pub(super) fn is_empty(&self) -> bool {
|
pub(super) fn is_empty(&self) -> bool {
|
||||||
self.iter.as_slice().is_empty()
|
self.iter.as_slice().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.iter.as_slice().len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for IpAddrs {
|
impl Iterator for IpAddrs {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use futures_util::{TryFutureExt, FutureExt};
|
|||||||
use net2::TcpBuilder;
|
use net2::TcpBuilder;
|
||||||
use tokio_net::driver::Handle;
|
use tokio_net::driver::Handle;
|
||||||
use tokio_net::tcp::TcpStream;
|
use tokio_net::tcp::TcpStream;
|
||||||
use tokio_timer::Delay;
|
use tokio_timer::{Delay, Timeout};
|
||||||
|
|
||||||
use crate::common::{Future, Pin, Poll, task};
|
use crate::common::{Future, Pin, Poll, task};
|
||||||
use super::{Connect, Connected, Destination};
|
use super::{Connect, Connected, Destination};
|
||||||
@@ -32,6 +32,7 @@ type ConnectFuture = Pin<Box<dyn Future<Output = io::Result<TcpStream>> + Send>>
|
|||||||
pub struct HttpConnector<R = GaiResolver> {
|
pub struct HttpConnector<R = GaiResolver> {
|
||||||
enforce_http: bool,
|
enforce_http: bool,
|
||||||
handle: Option<Handle>,
|
handle: Option<Handle>,
|
||||||
|
connect_timeout: Option<Duration>,
|
||||||
happy_eyeballs_timeout: Option<Duration>,
|
happy_eyeballs_timeout: Option<Duration>,
|
||||||
keep_alive_timeout: Option<Duration>,
|
keep_alive_timeout: Option<Duration>,
|
||||||
local_address: Option<IpAddr>,
|
local_address: Option<IpAddr>,
|
||||||
@@ -101,6 +102,7 @@ impl<R> HttpConnector<R> {
|
|||||||
HttpConnector {
|
HttpConnector {
|
||||||
enforce_http: true,
|
enforce_http: true,
|
||||||
handle: None,
|
handle: None,
|
||||||
|
connect_timeout: None,
|
||||||
happy_eyeballs_timeout: Some(Duration::from_millis(300)),
|
happy_eyeballs_timeout: Some(Duration::from_millis(300)),
|
||||||
keep_alive_timeout: None,
|
keep_alive_timeout: None,
|
||||||
local_address: None,
|
local_address: None,
|
||||||
@@ -168,6 +170,17 @@ impl<R> HttpConnector<R> {
|
|||||||
self.local_address = addr;
|
self.local_address = addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the connect timeout.
|
||||||
|
///
|
||||||
|
/// If a domain resolves to multiple IP addresses, the timeout will be
|
||||||
|
/// evenly divided across them.
|
||||||
|
///
|
||||||
|
/// Default is `None`.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_connect_timeout(&mut self, dur: Option<Duration>) {
|
||||||
|
self.connect_timeout = dur;
|
||||||
|
}
|
||||||
|
|
||||||
/// Set timeout for [RFC 6555 (Happy Eyeballs)][RFC 6555] algorithm.
|
/// Set timeout for [RFC 6555 (Happy Eyeballs)][RFC 6555] algorithm.
|
||||||
///
|
///
|
||||||
/// If hostname resolves to both IPv4 and IPv6 addresses and connection
|
/// If hostname resolves to both IPv4 and IPv6 addresses and connection
|
||||||
@@ -240,6 +253,7 @@ where
|
|||||||
HttpConnecting {
|
HttpConnecting {
|
||||||
state: State::Lazy(self.resolver.clone(), host.into(), self.local_address),
|
state: State::Lazy(self.resolver.clone(), host.into(), self.local_address),
|
||||||
handle: self.handle.clone(),
|
handle: self.handle.clone(),
|
||||||
|
connect_timeout: self.connect_timeout,
|
||||||
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
||||||
keep_alive_timeout: self.keep_alive_timeout,
|
keep_alive_timeout: self.keep_alive_timeout,
|
||||||
nodelay: self.nodelay,
|
nodelay: self.nodelay,
|
||||||
@@ -295,6 +309,7 @@ where
|
|||||||
let fut = HttpConnecting {
|
let fut = HttpConnecting {
|
||||||
state: State::Lazy(self.resolver.clone(), host.into(), self.local_address),
|
state: State::Lazy(self.resolver.clone(), host.into(), self.local_address),
|
||||||
handle: self.handle.clone(),
|
handle: self.handle.clone(),
|
||||||
|
connect_timeout: self.connect_timeout,
|
||||||
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
||||||
keep_alive_timeout: self.keep_alive_timeout,
|
keep_alive_timeout: self.keep_alive_timeout,
|
||||||
nodelay: self.nodelay,
|
nodelay: self.nodelay,
|
||||||
@@ -323,6 +338,7 @@ fn invalid_url<R: Resolve>(err: InvalidUrl, handle: &Option<Handle>) -> HttpConn
|
|||||||
keep_alive_timeout: None,
|
keep_alive_timeout: None,
|
||||||
nodelay: false,
|
nodelay: false,
|
||||||
port: 0,
|
port: 0,
|
||||||
|
connect_timeout: None,
|
||||||
happy_eyeballs_timeout: None,
|
happy_eyeballs_timeout: None,
|
||||||
reuse_address: false,
|
reuse_address: false,
|
||||||
send_buffer_size: None,
|
send_buffer_size: None,
|
||||||
@@ -357,6 +373,7 @@ impl StdError for InvalidUrl {
|
|||||||
pub struct HttpConnecting<R: Resolve = GaiResolver> {
|
pub struct HttpConnecting<R: Resolve = GaiResolver> {
|
||||||
state: State<R>,
|
state: State<R>,
|
||||||
handle: Option<Handle>,
|
handle: Option<Handle>,
|
||||||
|
connect_timeout: Option<Duration>,
|
||||||
happy_eyeballs_timeout: Option<Duration>,
|
happy_eyeballs_timeout: Option<Duration>,
|
||||||
keep_alive_timeout: Option<Duration>,
|
keep_alive_timeout: Option<Duration>,
|
||||||
nodelay: bool,
|
nodelay: bool,
|
||||||
@@ -389,7 +406,7 @@ where
|
|||||||
// skip resolving the dns and start connecting right away.
|
// skip resolving the dns and start connecting right away.
|
||||||
if let Some(addrs) = dns::IpAddrs::try_parse(host, me.port) {
|
if let Some(addrs) = dns::IpAddrs::try_parse(host, me.port) {
|
||||||
state = State::Connecting(ConnectingTcp::new(
|
state = State::Connecting(ConnectingTcp::new(
|
||||||
local_addr, addrs, me.happy_eyeballs_timeout, me.reuse_address));
|
local_addr, addrs, me.connect_timeout, me.happy_eyeballs_timeout, me.reuse_address));
|
||||||
} else {
|
} else {
|
||||||
let name = dns::Name::new(mem::replace(host, String::new()));
|
let name = dns::Name::new(mem::replace(host, String::new()));
|
||||||
state = State::Resolving(resolver.resolve(name), local_addr);
|
state = State::Resolving(resolver.resolve(name), local_addr);
|
||||||
@@ -403,7 +420,7 @@ where
|
|||||||
.collect();
|
.collect();
|
||||||
let addrs = dns::IpAddrs::new(addrs);
|
let addrs = dns::IpAddrs::new(addrs);
|
||||||
state = State::Connecting(ConnectingTcp::new(
|
state = State::Connecting(ConnectingTcp::new(
|
||||||
local_addr, addrs, me.happy_eyeballs_timeout, me.reuse_address));
|
local_addr, addrs, me.connect_timeout, me.happy_eyeballs_timeout, me.reuse_address));
|
||||||
},
|
},
|
||||||
State::Connecting(ref mut c) => {
|
State::Connecting(ref mut c) => {
|
||||||
let sock = ready!(c.poll(cx, &me.handle))?;
|
let sock = ready!(c.poll(cx, &me.handle))?;
|
||||||
@@ -454,6 +471,7 @@ impl ConnectingTcp {
|
|||||||
fn new(
|
fn new(
|
||||||
local_addr: Option<IpAddr>,
|
local_addr: Option<IpAddr>,
|
||||||
remote_addrs: dns::IpAddrs,
|
remote_addrs: dns::IpAddrs,
|
||||||
|
connect_timeout: Option<Duration>,
|
||||||
fallback_timeout: Option<Duration>,
|
fallback_timeout: Option<Duration>,
|
||||||
reuse_address: bool,
|
reuse_address: bool,
|
||||||
) -> ConnectingTcp {
|
) -> ConnectingTcp {
|
||||||
@@ -462,7 +480,7 @@ impl ConnectingTcp {
|
|||||||
if fallback_addrs.is_empty() {
|
if fallback_addrs.is_empty() {
|
||||||
return ConnectingTcp {
|
return ConnectingTcp {
|
||||||
local_addr,
|
local_addr,
|
||||||
preferred: ConnectingTcpRemote::new(preferred_addrs),
|
preferred: ConnectingTcpRemote::new(preferred_addrs, connect_timeout),
|
||||||
fallback: None,
|
fallback: None,
|
||||||
reuse_address,
|
reuse_address,
|
||||||
};
|
};
|
||||||
@@ -470,17 +488,17 @@ impl ConnectingTcp {
|
|||||||
|
|
||||||
ConnectingTcp {
|
ConnectingTcp {
|
||||||
local_addr,
|
local_addr,
|
||||||
preferred: ConnectingTcpRemote::new(preferred_addrs),
|
preferred: ConnectingTcpRemote::new(preferred_addrs, connect_timeout),
|
||||||
fallback: Some(ConnectingTcpFallback {
|
fallback: Some(ConnectingTcpFallback {
|
||||||
delay: tokio_timer::delay_for(fallback_timeout),
|
delay: tokio_timer::delay_for(fallback_timeout),
|
||||||
remote: ConnectingTcpRemote::new(fallback_addrs),
|
remote: ConnectingTcpRemote::new(fallback_addrs, connect_timeout),
|
||||||
}),
|
}),
|
||||||
reuse_address,
|
reuse_address,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ConnectingTcp {
|
ConnectingTcp {
|
||||||
local_addr,
|
local_addr,
|
||||||
preferred: ConnectingTcpRemote::new(remote_addrs),
|
preferred: ConnectingTcpRemote::new(remote_addrs, connect_timeout),
|
||||||
fallback: None,
|
fallback: None,
|
||||||
reuse_address,
|
reuse_address,
|
||||||
}
|
}
|
||||||
@@ -495,13 +513,17 @@ struct ConnectingTcpFallback {
|
|||||||
|
|
||||||
struct ConnectingTcpRemote {
|
struct ConnectingTcpRemote {
|
||||||
addrs: dns::IpAddrs,
|
addrs: dns::IpAddrs,
|
||||||
|
connect_timeout: Option<Duration>,
|
||||||
current: Option<ConnectFuture>,
|
current: Option<ConnectFuture>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectingTcpRemote {
|
impl ConnectingTcpRemote {
|
||||||
fn new(addrs: dns::IpAddrs) -> Self {
|
fn new(addrs: dns::IpAddrs, connect_timeout: Option<Duration>) -> Self {
|
||||||
|
let connect_timeout = connect_timeout.map(|t| t / (addrs.len() as u32));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
addrs,
|
addrs,
|
||||||
|
connect_timeout,
|
||||||
current: None,
|
current: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,14 +552,14 @@ impl ConnectingTcpRemote {
|
|||||||
err = Some(e);
|
err = Some(e);
|
||||||
if let Some(addr) = self.addrs.next() {
|
if let Some(addr) = self.addrs.next() {
|
||||||
debug!("connecting to {}", addr);
|
debug!("connecting to {}", addr);
|
||||||
*current = connect(&addr, local_addr, handle, reuse_address)?;
|
*current = connect(&addr, local_addr, handle, reuse_address, self.connect_timeout)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(addr) = self.addrs.next() {
|
} else if let Some(addr) = self.addrs.next() {
|
||||||
debug!("connecting to {}", addr);
|
debug!("connecting to {}", addr);
|
||||||
self.current = Some(connect(&addr, local_addr, handle, reuse_address)?);
|
self.current = Some(connect(&addr, local_addr, handle, reuse_address, self.connect_timeout)?);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +568,13 @@ impl ConnectingTcpRemote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(addr: &SocketAddr, local_addr: &Option<IpAddr>, handle: &Option<Handle>, reuse_address: bool) -> io::Result<ConnectFuture> {
|
fn connect(
|
||||||
|
addr: &SocketAddr,
|
||||||
|
local_addr: &Option<IpAddr>,
|
||||||
|
handle: &Option<Handle>,
|
||||||
|
reuse_address: bool,
|
||||||
|
connect_timeout: Option<Duration>,
|
||||||
|
) -> io::Result<ConnectFuture> {
|
||||||
let builder = match addr {
|
let builder = match addr {
|
||||||
&SocketAddr::V4(_) => TcpBuilder::new_v4()?,
|
&SocketAddr::V4(_) => TcpBuilder::new_v4()?,
|
||||||
&SocketAddr::V6(_) => TcpBuilder::new_v6()?,
|
&SocketAddr::V6(_) => TcpBuilder::new_v6()?,
|
||||||
@@ -581,10 +609,16 @@ fn connect(addr: &SocketAddr, local_addr: &Option<IpAddr>, handle: &Option<Handl
|
|||||||
let std_tcp = builder.to_tcp_stream()?;
|
let std_tcp = builder.to_tcp_stream()?;
|
||||||
|
|
||||||
Ok(Box::pin(async move {
|
Ok(Box::pin(async move {
|
||||||
TcpStream::connect_std(std_tcp, &addr, &handle).await
|
let connect = TcpStream::connect_std(std_tcp, &addr, &handle);
|
||||||
|
match connect_timeout {
|
||||||
|
Some(timeout) => match Timeout::new(connect, timeout).await {
|
||||||
|
Ok(Ok(s)) => Ok(s),
|
||||||
|
Ok(Err(e)) => Err(e),
|
||||||
|
Err(e) => Err(io::Error::new(io::ErrorKind::TimedOut, e)),
|
||||||
|
}
|
||||||
|
None => connect.await,
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//Ok(Box::pin(TcpStream::connect_std(std_tcp, addr, &handle)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectingTcp {
|
impl ConnectingTcp {
|
||||||
@@ -673,7 +707,6 @@ mod tests {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_errors_missing_scheme() {
|
fn test_errors_missing_scheme() {
|
||||||
let mut rt = Runtime::new().unwrap();
|
let mut rt = Runtime::new().unwrap();
|
||||||
@@ -765,7 +798,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let addrs = hosts.iter().map(|host| (host.clone(), addr.port()).into()).collect();
|
let addrs = hosts.iter().map(|host| (host.clone(), addr.port()).into()).collect();
|
||||||
let connecting_tcp = ConnectingTcp::new(None, dns::IpAddrs::new(addrs), Some(fallback_timeout), false);
|
let connecting_tcp = ConnectingTcp::new(None, dns::IpAddrs::new(addrs), None, Some(fallback_timeout), false);
|
||||||
let fut = ConnectingTcpFuture(connecting_tcp);
|
let fut = ConnectingTcpFuture(connecting_tcp);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|||||||
Reference in New Issue
Block a user