use crate::proto;
use crate::security::{Conn, Connector, NO_SENDER, Purpose, Raw, Security};
use fusee_core::frame;
use std::io::{self, Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::{Duration, Instant};
pub(crate) const REPLICAS: &str = "replicas://";
enum Target {
Tcp(String),
Unix(String),
Replicas(Vec<String>),
}
pub(crate) struct Endpoint {
target: Target,
connector: Connector,
max_shards: u32,
shm: bool,
invocation: Option<u64>,
}
impl Endpoint {
pub(crate) fn parse(address: &str) -> io::Result<Self> {
let (target, options) = match address.split_once('?') {
Some((target, options)) => (target, options),
None => (address, ""),
};
let mut max_shards = None;
let mut shm = None;
let mut invocation = None;
let mut security = Vec::new();
for pair in options.split('&').filter(|p| !p.is_empty()) {
if let Some(v) = pair.strip_prefix("invocation=") {
let id = crate::invoke::parse_id(v).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invocation takes the request's invocation ID, not '{v}'"),
)
})?;
if invocation.replace(id).is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"connection option 'invocation' is given twice",
));
}
continue;
}
if let Some(v) = pair.strip_prefix("shm=") {
let on = match v {
"1" => true,
"0" => false,
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("shm takes 1 or 0, not '{v}'"),
));
}
};
if shm.replace(on).is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"connection option 'shm' is given twice",
));
}
continue;
}
match pair.strip_prefix("max-shards=") {
Some(v) => {
let n = v.parse::<u32>().ok().filter(|n| *n > 0).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("max-shards needs a positive number, not '{v}'"),
)
})?;
if max_shards.replace(n).is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"connection option 'max-shards' is given twice",
));
}
}
None => security.push(pair),
}
}
let mut endpoint = Self::new(target, &Security::from_options(&security.join("&"))?)?;
endpoint.max_shards = max_shards.unwrap_or(0);
endpoint.shm = shm.unwrap_or(false);
endpoint.invocation = invocation;
if endpoint.shm && !matches!(endpoint.target, Target::Unix(_)) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"shm=1 needs a unix: address (shared memory is for workers on the server's host)",
));
}
Ok(endpoint)
}
pub(crate) fn max_shards(&self) -> u32 {
self.max_shards
}
pub(crate) fn shm(&self) -> bool {
self.shm
}
pub(crate) fn invocation(&self) -> Option<u64> {
self.invocation
}
pub(crate) fn with_security(address: &str, security: &Security) -> io::Result<Self> {
if address.contains('?') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"give connection options in the address or as Security, not both",
));
}
Self::new(address, security)
}
fn new(address: &str, security: &Security) -> io::Result<Self> {
let target = if let Some(path) = address.strip_prefix("unix:") {
if security.tls.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"TLS is not used over Unix sockets; drop ca=, cert= and key=",
));
}
Target::Unix(path.to_owned())
} else if let Some(list) = address.strip_prefix(REPLICAS) {
let names: Vec<String> = list.split(',').map(str::to_owned).collect();
if !matches!(names.len(), 3 | 5) || names.iter().any(|name| name.is_empty()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected three or five replica worker addresses",
));
}
Target::Replicas(names)
} else {
Target::Tcp(address.to_owned())
};
Ok(Self {
target,
connector: Connector::new(security, Purpose::Server, NO_SENDER)?,
max_shards: 0,
shm: false,
invocation: None,
})
}
pub(crate) fn is_replicas(&self) -> bool {
matches!(self.target, Target::Replicas(_))
}
pub(crate) fn connect(&self) -> io::Result<Conn> {
match &self.target {
Target::Unix(path) => {
let raw = Raw::Unix(
std::os::unix::net::UnixStream::connect(path)
.map_err(|e| no_server(&format!("unix:{path}"), e))?,
);
self.secure(raw, path, crate::security::HANDSHAKE_TIMEOUT)
}
Target::Tcp(address) => {
let tcp = TcpStream::connect(address).map_err(|e| no_server(address, e))?;
tcp.set_nodelay(true)?;
self.secure(Raw::Tcp(tcp), address, crate::security::HANDSHAKE_TIMEOUT)
}
Target::Replicas(names) => self.connect_replicas(names),
}
}
fn secure(&self, raw: Raw, address: &str, timeout: Duration) -> io::Result<Conn> {
if self.connector.is_open() {
return Conn::plain(raw);
}
raw.set_timeouts(Some(timeout))?;
let conn = self.connector.secure(raw, address)?;
conn.raw().set_timeouts(None)?;
Ok(conn)
}
fn connect_replicas(&self, names: &[String]) -> io::Result<Conn> {
let addrs = names
.iter()
.map(|name| {
name.to_socket_addrs()
.map(|v| v.map(|a| (name.as_str(), a)).collect::<Vec<_>>())
})
.collect::<io::Result<Vec<_>>>()?;
let deadline = Instant::now() + Duration::from_secs(15);
let timeout = Duration::from_millis(750);
loop {
for (name, addr) in addrs.iter().flatten() {
let error = match TcpStream::connect_timeout(addr, timeout)
.and_then(|tcp| {
tcp.set_nodelay(true)?;
self.secure(Raw::Tcp(tcp), name, timeout)
})
.and_then(|mut conn| probe(&mut conn, timeout).map(|()| conn))
{
Ok(conn) => return Ok(conn),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => return Err(e),
Err(e) => e,
};
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("no worker endpoint has a committed quorum (last error: {error})"),
));
}
}
if addrs.iter().all(Vec::is_empty) {
return Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
"replica worker addresses did not resolve",
));
}
std::thread::sleep(Duration::from_millis(50));
}
}
}
fn probe(stream: &mut Conn, timeout: Duration) -> io::Result<()> {
stream.raw().set_timeouts(Some(timeout))?;
let mut out = Vec::new();
frame::encode(proto::req::QUORUM_PROBE, &[], &mut out);
stream.write_all(&out)?;
let mut expected = Vec::new();
frame::encode(proto::resp::QUORUM_READY, &[], &mut expected);
let mut response = vec![0; expected.len()];
stream.read_exact(&mut response)?;
if response != expected {
if response[..4] == frame::MAGIC[..] && response[4] == proto::resp::ERR {
let len = u32::from_le_bytes([response[5], response[6], response[7], response[8]]);
let mut body = vec![0; len.min(4096) as usize];
stream.read_exact(&mut body)?;
let mut r = fusee_core::wire::Reader::new(&body);
let _ = r.u64();
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"the server refused the connection: {}",
r.str().unwrap_or("")
),
));
}
return Err(io::Error::other("unexpected quorum probe response"));
}
stream.raw().set_timeouts(None)
}
pub(crate) fn no_server(address: &str, e: io::Error) -> io::Error {
let hint = match e.kind() {
io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound => format!(
". Start one with: fusee server --dir <data dir> --listen {}",
address.trim_start_matches("unix:")
),
_ => String::new(),
};
io::Error::new(
e.kind(),
format!("no fusee server at {address} ({e}){hint}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn max_shards_is_a_connection_option_next_to_security() {
assert_eq!(Endpoint::parse("127.0.0.1:1").unwrap().max_shards(), 0);
assert_eq!(
Endpoint::parse("127.0.0.1:1?max-shards=3")
.unwrap()
.max_shards(),
3
);
for bad in [
"127.0.0.1:1?max-shards=0",
"127.0.0.1:1?max-shards=x",
"127.0.0.1:1?max-shards=",
"127.0.0.1:1?max-shards=1&max-shards=2",
"127.0.0.1:1?max-shards=1&shards=2",
"127.0.0.1:1?shm=1",
"unix:/tmp/x.sock?shm=yes",
"unix:/tmp/x.sock?shm=1&shm=0",
] {
assert!(Endpoint::parse(bad).is_err(), "{bad}");
}
assert_eq!(
Endpoint::parse("127.0.0.1:1?invocation=00000000000000ab")
.unwrap()
.invocation(),
Some(0xab)
);
assert_eq!(Endpoint::parse("127.0.0.1:1").unwrap().invocation(), None);
for bad in [
"127.0.0.1:1?invocation=ab",
"127.0.0.1:1?invocation=0000000000000000",
"127.0.0.1:1?invocation=00000000000000ab&invocation=00000000000000ab",
] {
assert!(Endpoint::parse(bad).is_err(), "{bad}");
}
assert!(Endpoint::parse("unix:/tmp/x.sock?shm=1").unwrap().shm());
assert!(!Endpoint::parse("unix:/tmp/x.sock?shm=0").unwrap().shm());
assert!(!Endpoint::parse("unix:/tmp/x.sock").unwrap().shm());
}
}