use std::collections::HashMap;
use std::fmt;
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
pub struct NodeIdentity {
pub name: String,
pub host: String,
pub port: u16,
pub incarnation: u64,
}
impl NodeIdentity {
pub fn new(name: impl Into<String>, host: impl Into<String>, port: u16) -> Self {
Self {
name: name.into(),
host: host.into(),
port,
incarnation: rand::random(),
}
}
pub fn socket_addr(&self) -> SocketAddr {
let ip: std::net::IpAddr = self.host.parse().unwrap_or([127, 0, 0, 1].into());
SocketAddr::new(ip, self.port)
}
pub fn node_id_string(&self) -> String {
format!(
"{}@{}:{}#{}",
self.name, self.host, self.port, self.incarnation
)
}
pub fn display_id(&self) -> String {
format!("{}@{}:{}", self.name, self.host, self.port)
}
}
impl fmt::Display for NodeIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}@{}:{}#{}",
self.name, self.host, self.port, self.incarnation
)
}
}
impl PartialEq for NodeIdentity {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.host == other.host
&& self.port == other.port
&& self.incarnation == other.incarnation
}
}
impl std::hash::Hash for NodeIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.host.hash(state);
self.port.hash(state);
self.incarnation.hash(state);
}
}
impl foca::Identity for NodeIdentity {
type Addr = SocketAddr;
fn addr(&self) -> SocketAddr {
self.socket_addr()
}
fn renew(&self) -> Option<Self> {
Some(Self {
name: self.name.clone(),
host: self.host.clone(),
port: self.port,
incarnation: self.incarnation.wrapping_add(1),
})
}
fn win_addr_conflict(&self, other: &Self) -> bool {
self.incarnation > other.incarnation
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum NodeClass {
#[default]
Worker,
Coordinator,
Edge,
Custom(String),
}
impl std::fmt::Display for NodeClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Worker => write!(f, "worker"),
Self::Coordinator => write!(f, "coordinator"),
Self::Edge => write!(f, "edge"),
Self::Custom(name) => write!(f, "custom({name})"),
}
}
}
#[derive(Debug, Clone)]
pub struct TransportTuning {
pub initial_rtt_ms: u64,
pub max_idle_timeout_secs: u64,
pub keep_alive_interval_secs: Option<u64>,
pub max_concurrent_bidi_streams: u32,
pub stream_receive_window: u32,
pub receive_window: u32,
pub send_window: u64,
pub initial_mtu: u16,
}
impl Default for TransportTuning {
fn default() -> Self {
Self {
initial_rtt_ms: 1,
max_idle_timeout_secs: 30,
keep_alive_interval_secs: Some(5),
max_concurrent_bidi_streams: 1024,
stream_receive_window: 256 * 1024, receive_window: 2 * 1024 * 1024, send_window: 2 * 1024 * 1024, initial_mtu: 1452,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum Discovery {
Mdns { service_name: String },
SeedNodes(Vec<SocketAddr>),
Both {
service_name: String,
seed_nodes: Vec<SocketAddr>,
},
#[default]
None,
}
#[derive(Debug, Clone)]
pub struct ClusterConfig {
pub identity: NodeIdentity,
pub listen_addr: SocketAddr,
pub cookie: String,
pub discovery: Discovery,
pub node_class: NodeClass,
pub node_metadata: HashMap<String, String>,
pub transport: TransportTuning,
}
pub struct ClusterConfigBuilder {
name: Option<String>,
listen_addr: Option<SocketAddr>,
advertise_addr: Option<SocketAddr>,
cookie: Option<String>,
discovery: Discovery,
node_class: NodeClass,
node_metadata: HashMap<String, String>,
transport: TransportTuning,
}
impl ClusterConfigBuilder {
pub fn new() -> Self {
Self {
name: None,
listen_addr: None,
advertise_addr: None,
cookie: None,
discovery: Discovery::default(),
node_class: NodeClass::default(),
node_metadata: HashMap::new(),
transport: TransportTuning::default(),
}
}
pub fn advertise(mut self, addr: impl Into<SocketAddr>) -> Self {
self.advertise_addr = Some(addr.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn listen(mut self, addr: impl Into<SocketAddr>) -> Self {
self.listen_addr = Some(addr.into());
self
}
pub fn cookie(mut self, cookie: impl Into<String>) -> Self {
self.cookie = Some(cookie.into());
self
}
pub fn seed_nodes(mut self, seeds: impl IntoIterator<Item = SocketAddr>) -> Self {
let seeds: Vec<SocketAddr> = seeds.into_iter().collect();
self.discovery = match self.discovery {
Discovery::Mdns { service_name } => Discovery::Both {
service_name,
seed_nodes: seeds,
},
_ => Discovery::SeedNodes(seeds),
};
self
}
pub fn discovery(mut self, discovery: Discovery) -> Self {
self.discovery = discovery;
self
}
pub fn node_class(mut self, class: NodeClass) -> Self {
self.node_class = class;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.node_metadata.insert(key.into(), value.into());
self
}
pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.node_metadata = metadata;
self
}
pub fn transport(mut self, tuning: TransportTuning) -> Self {
self.transport = tuning;
self
}
pub fn build(self) -> Result<ClusterConfig, &'static str> {
let listen_addr = self.listen_addr.ok_or("listen address is required")?;
let cookie = self.cookie.ok_or("cookie is required")?;
let peer_addr = self.advertise_addr.unwrap_or(listen_addr);
let name = self.name.unwrap_or_else(|| {
let host = peer_addr.ip();
let port = peer_addr.port();
format!("{host}-{port}")
});
let identity = NodeIdentity::new(name, peer_addr.ip().to_string(), peer_addr.port());
Ok(ClusterConfig {
identity,
listen_addr,
cookie,
discovery: self.discovery,
node_class: self.node_class,
node_metadata: self.node_metadata,
transport: self.transport,
})
}
}
impl Default for ClusterConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl ClusterConfig {
pub fn builder() -> ClusterConfigBuilder {
ClusterConfigBuilder::new()
}
}