use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::sync::Arc;
use std::{fmt, io, time};
use crate::config::Config;
use crate::unversioned::transport::time::Instant;
use crate::util::IoResultExt;
use crate::{Error, Timeout};
use super::ResolvedSocketAddrs;
use super::chain::Either;
use super::time::Duration;
use super::{Buffers, ConnectionDetails, Connector, LazyBuffers, NextTimeout, Transport};
#[derive(Default)]
pub struct TcpConnector(());
impl<In: Transport> Connector<In> for TcpConnector {
type Out = Either<In, TcpTransport>;
fn connect(
&self,
details: &ConnectionDetails,
chained: Option<In>,
) -> Result<Option<Self::Out>, Error> {
if chained.is_some() {
trace!("Skip");
return Ok(chained.map(Either::A));
}
let config = &details.config;
let stream = try_connect(
&details.addrs,
details.now,
details.timeout,
details.current_time.clone(),
config,
)?;
let buffers = LazyBuffers::new(config.input_buffer_size(), config.output_buffer_size());
let transport = TcpTransport::new(stream, buffers);
Ok(Some(Either::B(transport)))
}
}
fn try_connect(
addrs: &ResolvedSocketAddrs,
start: Instant,
timeout: NextTimeout,
current_time: Arc<dyn Fn() -> Instant + Send + Sync + 'static>,
config: &Config,
) -> Result<TcpStream, Error> {
try_connect_with(addrs, start, timeout, current_time, |addr, per_addr| {
try_connect_single(addr, per_addr, config)
})
}
fn try_connect_with<T>(
addrs: &ResolvedSocketAddrs,
start: Instant,
timeout: NextTimeout,
current_time: Arc<dyn Fn() -> Instant + Send + Sync + 'static>,
mut connect: impl FnMut(SocketAddr, Option<Duration>) -> Result<T, Error>,
) -> Result<T, Error> {
const MIN_PER_ADDRESS_TIMEOUT: Duration = Duration::from_millis(10);
let num_addrs = addrs.len();
let total_weight = 2.0 * (1.0 - 0.5_f64.powi(num_addrs as i32));
let mut weight = 1.0_f64;
let mut last_err: Option<Error> = None;
for addr in addrs {
let per_addr = timeout.not_zero().map(|t| {
let secs = t.as_secs_f64() * weight / total_weight;
let timeout = Duration::from_millis((secs * 1000.0) as u64);
timeout.max(MIN_PER_ADDRESS_TIMEOUT)
});
match connect(*addr, per_addr) {
Ok(v) => return Ok(v),
Err(Error::Io(e)) if is_addr_specific_error(&e) => {
trace!("{} failed: {}", addr, e);
last_err = Some(Error::Io(e));
continue;
}
Err(e @ Error::Timeout(_)) => {
let elapsed = current_time().duration_since(start);
if elapsed > timeout.after {
return Err(Error::Timeout(timeout.reason));
}
last_err = Some(e);
}
Err(e) => return Err(e),
}
weight /= 2.0;
}
debug!("Failed to connect to any resolved address");
Err(last_err.unwrap_or_else(|| {
Error::Io(io::Error::new(
io::ErrorKind::ConnectionRefused,
"Connection refused",
))
}))
}
fn is_addr_specific_error(e: &io::Error) -> bool {
#[cfg(windows)]
const WSAEACCES: i32 = 10013;
#[cfg(windows)]
if e.raw_os_error() == Some(WSAEACCES) {
return true;
}
matches!(
e.kind(),
io::ErrorKind::ConnectionRefused
| io::ErrorKind::HostUnreachable
| io::ErrorKind::NetworkUnreachable
| io::ErrorKind::AddrNotAvailable
)
}
fn try_connect_single(
addr: SocketAddr,
per_addr: Option<Duration>,
config: &Config,
) -> Result<TcpStream, Error> {
trace!("Try connect TcpStream to {}", addr);
let maybe_stream = if let Some(when) = per_addr {
TcpStream::connect_timeout(&addr, *when)
} else {
TcpStream::connect(addr)
}
.normalize_would_block();
let stream = match maybe_stream {
Ok(v) => v,
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
return Err(Error::Timeout(Timeout::Connect));
}
Err(e) => return Err(e.into()),
};
if config.no_delay() {
stream.set_nodelay(true)?;
}
debug!("Connected TcpStream to {}", addr);
Ok(stream)
}
pub struct TcpTransport {
stream: TcpStream,
buffers: LazyBuffers,
timeout_write: Option<Duration>,
timeout_read: Option<Duration>,
}
impl TcpTransport {
pub fn new(stream: TcpStream, buffers: LazyBuffers) -> TcpTransport {
TcpTransport {
stream,
buffers,
timeout_read: None,
timeout_write: None,
}
}
}
fn maybe_update_timeout(
timeout: NextTimeout,
previous: &mut Option<Duration>,
stream: &TcpStream,
f: impl Fn(&TcpStream, Option<time::Duration>) -> io::Result<()>,
) -> io::Result<()> {
let maybe_timeout = timeout.not_zero();
if maybe_timeout != *previous {
(f)(stream, maybe_timeout.map(|t| *t))?;
*previous = maybe_timeout;
}
Ok(())
}
impl Transport for TcpTransport {
fn buffers(&mut self) -> &mut dyn Buffers {
&mut self.buffers
}
fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> {
maybe_update_timeout(
timeout,
&mut self.timeout_write,
&self.stream,
TcpStream::set_write_timeout,
)?;
let output = &self.buffers.output()[..amount];
match self.stream.write_all(output).normalize_would_block() {
Ok(v) => Ok(v),
Err(e) if e.kind() == io::ErrorKind::TimedOut => Err(Error::Timeout(timeout.reason)),
Err(e) => Err(e.into()),
}?;
Ok(())
}
fn await_input(&mut self, timeout: NextTimeout) -> Result<bool, Error> {
maybe_update_timeout(
timeout,
&mut self.timeout_read,
&self.stream,
TcpStream::set_read_timeout,
)?;
let input = self.buffers.input_append_buf();
let amount = match self.stream.read(input).normalize_would_block() {
Ok(v) => Ok(v),
Err(e) if e.kind() == io::ErrorKind::TimedOut => Err(Error::Timeout(timeout.reason)),
Err(e) => Err(e.into()),
}?;
self.buffers.input_appended(amount);
Ok(amount > 0)
}
fn is_open(&mut self) -> bool {
probe_tcp_stream(&mut self.stream).unwrap_or(false)
}
}
fn probe_tcp_stream(stream: &mut TcpStream) -> Result<bool, Error> {
stream.set_nonblocking(true)?;
let mut buf = [0];
match stream.read(&mut buf) {
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
}
Ok(_) => {
debug!("Unexpected bytes from server. Closing connection");
return Ok(false);
}
Err(_) => return Ok(false),
};
stream.set_nonblocking(false)?;
Ok(true)
}
impl fmt::Debug for TcpConnector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TcpConnector").finish()
}
}
impl fmt::Debug for TcpTransport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TcpTransport")
.field("addr", &self.stream.peer_addr().ok())
.finish()
}
}
#[cfg(test)]
mod test {
use super::*;
fn scripted_connect(
outcomes: Vec<Result<(), Error>>,
elapsed: Duration,
) -> (Result<(), Error>, usize) {
let mut addrs = ResolvedSocketAddrs::from_fn(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
for i in 0..outcomes.len() {
addrs.push(SocketAddr::from(([127, 0, 0, 1], 10000 + i as u16)));
}
let start = Instant::now();
let mut outcomes = outcomes.into_iter();
let mut attempts = 0;
let result = try_connect_with(
&addrs,
start,
NextTimeout {
after: Duration::from_secs(10),
reason: Timeout::Global,
},
Arc::new(move || start + elapsed),
|addr, _| {
assert_eq!(addr, addrs[attempts]);
attempts += 1;
outcomes.next().unwrap()
},
);
(result, attempts)
}
fn io_error(kind: io::ErrorKind) -> Result<(), Error> {
Err(io::Error::from(kind).into())
}
#[test]
fn timeout_replaces_earlier_unreachable_error() {
let (result, attempts) = scripted_connect(
vec![
io_error(io::ErrorKind::NetworkUnreachable),
Err(Error::Timeout(Timeout::Connect)),
],
Duration::from_secs(6),
);
assert_eq!(attempts, 2);
assert!(matches!(result, Err(Error::Timeout(Timeout::Connect))));
}
#[test]
fn addr_specific_failures_and_timeout_fall_back_to_success() {
for failure in [
io_error(io::ErrorKind::ConnectionRefused),
io_error(io::ErrorKind::HostUnreachable),
io_error(io::ErrorKind::NetworkUnreachable),
io_error(io::ErrorKind::AddrNotAvailable),
Err(Error::Timeout(Timeout::Connect)),
] {
let (result, attempts) = scripted_connect(
vec![failure, Ok(()), io_error(io::ErrorKind::PermissionDenied)],
Duration::from_secs(1),
);
assert!(result.is_ok());
assert_eq!(attempts, 2);
}
}
#[test]
fn last_io_error_replaces_timeout_and_preserves_details() {
let (result, attempts) = scripted_connect(
vec![
Err(Error::Timeout(Timeout::Connect)),
Err(io::Error::new(io::ErrorKind::HostUnreachable, "last address").into()),
],
Duration::from_secs(1),
);
assert_eq!(attempts, 2);
let Err(Error::Io(error)) = result else {
panic!("expected last I/O error: {result:?}");
};
assert_eq!(error.kind(), io::ErrorKind::HostUnreachable);
assert_eq!(error.to_string(), "last address");
}
#[test]
fn all_timeouts_report_connect_timeout() {
let (result, attempts) = scripted_connect(
vec![
Err(Error::Timeout(Timeout::Connect)),
Err(Error::Timeout(Timeout::Connect)),
],
Duration::from_secs(6),
);
assert_eq!(attempts, 2);
assert!(matches!(result, Err(Error::Timeout(Timeout::Connect))));
}
#[test]
fn overall_timeout_takes_precedence_and_stops_attempts() {
let (result, attempts) = scripted_connect(
vec![
io_error(io::ErrorKind::NetworkUnreachable),
Err(Error::Timeout(Timeout::Connect)),
Ok(()),
],
Duration::from_secs(11),
);
assert_eq!(attempts, 2);
assert!(matches!(result, Err(Error::Timeout(Timeout::Global))));
}
#[test]
fn other_io_errors_stop_attempts() {
let (result, attempts) = scripted_connect(
vec![io_error(io::ErrorKind::PermissionDenied), Ok(())],
Duration::from_secs(1),
);
assert_eq!(attempts, 1);
assert!(matches!(result, Err(Error::Io(e)) if e.kind() == io::ErrorKind::PermissionDenied));
}
#[test]
fn empty_address_list_reports_refusal() {
let (result, attempts) = scripted_connect(vec![], Duration::from_secs(0));
assert_eq!(attempts, 0);
assert!(
matches!(result, Err(Error::Io(e)) if e.kind() == io::ErrorKind::ConnectionRefused)
);
}
#[test]
fn addr_specific_errors_try_the_next_addr() {
for kind in [
io::ErrorKind::ConnectionRefused,
io::ErrorKind::HostUnreachable,
io::ErrorKind::NetworkUnreachable,
io::ErrorKind::AddrNotAvailable,
] {
assert!(
is_addr_specific_error(&io::Error::from(kind)),
"{kind:?} should move on to the next address"
);
}
for kind in [io::ErrorKind::TimedOut, io::ErrorKind::PermissionDenied] {
assert!(
!is_addr_specific_error(&io::Error::from(kind)),
"{kind:?} should bail"
);
}
}
#[test]
#[cfg(windows)]
fn wsaeacces_tries_the_next_addr() {
assert!(is_addr_specific_error(&io::Error::from_raw_os_error(10013)));
}
}