use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::time::{Duration, Instant};
use serde_json::{json, Value};
use crate::config::cfg;
use crate::logfmt::log;
use crate::state::{NodeId, SharedRef};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Island {
pub nodes: Vec<NodeId>,
pub addr: BTreeMap<NodeId, String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Fabrics {
pub islands: Vec<Island>,
pub tagged: BTreeSet<NodeId>,
}
#[derive(Default, Debug)]
pub struct FabricView {
pub rdma: BTreeMap<NodeId, Vec<String>>,
pub ok_pairs: BTreeSet<(String, String)>,
}
impl FabricView {
fn linked(&self, a: &str, b: &str) -> bool {
self.ok_pairs.contains(&(a.to_string(), b.to_string()))
|| self.ok_pairs.contains(&(b.to_string(), a.to_string()))
}
fn ports(&self) -> Vec<(&NodeId, &String)> {
let mut out: Vec<(&NodeId, &String)> = self
.rdma
.iter()
.flat_map(|(n, addrs)| addrs.iter().map(move |a| (n, a)))
.collect();
out.sort();
out
}
fn joins(&self, x: (&NodeId, &String), y: (&NodeId, &String)) -> bool {
x.0 != y.0 && self.linked(x.1, y.1)
}
}
pub fn islands(v: &FabricView) -> Vec<Island> {
let ports = v.ports();
let mut out: Vec<Island> = Vec::new();
let mut placed: HashSet<(&NodeId, &String)> = HashSet::new();
for seed in &ports {
if placed.contains(seed) {
continue;
}
let mut comp: Vec<(&NodeId, &String)> = vec![*seed];
let mut queue = vec![*seed];
while let Some(p) = queue.pop() {
for q in &ports {
if !comp.contains(q) && v.joins(p, *q) {
comp.push(*q);
queue.push(*q);
}
}
}
for p in &comp {
placed.insert(*p);
}
loop {
let worst = comp
.iter()
.map(|p| {
let deg = comp.iter().filter(|q| v.joins(*p, **q)).count();
(deg, *p)
})
.min();
let Some((deg, port)) = worst else { break };
if deg + 1 >= comp.len() {
break;
}
log(
"island_pruned",
&[
("node", port.0.clone()),
("addr", port.1.clone()),
("reaches", deg.to_string()),
("of", (comp.len() - 1).to_string()),
(
"why",
"a placement group must fit a set every member reaches".to_string(),
),
],
);
comp.retain(|q| q != &port);
}
if comp.len() < 2 {
continue;
}
comp.sort();
out.push(Island {
nodes: comp.iter().map(|(n, _)| (*n).clone()).collect(),
addr: comp
.iter()
.map(|(n, a)| ((*n).clone(), (*a).clone()))
.collect(),
});
}
out.sort_by(|a, b| a.nodes.cmp(&b.nodes));
out
}
pub fn fabrics(v: &FabricView) -> Fabrics {
Fabrics {
islands: islands(v),
tagged: v.rdma.keys().cloned().collect(),
}
}
fn gather(shared: &SharedRef) -> FabricView {
let mut v = FabricView::default();
let mut note = |node: &str, addrs: Vec<String>, tags: BTreeMap<String, Vec<String>>| {
let tagged: Vec<String> = addrs
.into_iter()
.filter(|a| {
tags.get(a)
.map(|t| t.iter().any(|x| x == "rdma"))
.unwrap_or(false)
})
.collect();
if !tagged.is_empty() {
v.rdma.insert(node.to_string(), tagged);
}
};
let st = shared.st.lock().unwrap();
note(
&st.node_id,
crate::announce::local_addrs(),
crate::announce::local_addr_tags(),
);
for p in st.peers.values().filter(|p| p.alive) {
note(&p.node_id, p.addrs.clone(), p.addr_tags.clone());
for (local, remotes) in &p.probe_pairs {
for (remote, r) in remotes {
if r.ok {
v.ok_pairs.insert((local.clone(), remote.clone()));
}
}
}
for (_, q) in p.last_status["peers"].as_object().into_iter().flatten() {
let Some(id) = q["node_id"].as_str().or_else(|| q["id"].as_str()) else {
continue;
};
note(id, str_list(&q["addrs"]), tag_map(&q["addr_tags"]));
for (local, remotes) in q["probes"].as_object().into_iter().flatten() {
for (remote, r) in remotes.as_object().into_iter().flatten() {
if r["ok"].as_bool().unwrap_or(false) {
v.ok_pairs.insert((local.clone(), remote.clone()));
}
}
}
}
}
v
}
fn str_list(v: &Value) -> Vec<String> {
v.as_array()
.into_iter()
.flatten()
.filter_map(|a| a.as_str())
.map(str::to_string)
.collect()
}
fn tag_map(v: &Value) -> BTreeMap<String, Vec<String>> {
v.as_object()
.into_iter()
.flatten()
.map(|(k, t)| (k.clone(), str_list(t)))
.collect()
}
pub fn start(shared: SharedRef) {
std::thread::spawn(move || run(shared));
}
fn run(shared: SharedRef) {
let hold_down = Duration::from_millis(cfg().island_hold_down_ms);
let tick = Duration::from_millis((cfg().island_hold_down_ms / 5).clamp(100, 1000));
let mut candidate: Option<(Fabrics, Instant)> = None;
let mut unverified: HashSet<String> = HashSet::new();
loop {
std::thread::sleep(tick);
let view = gather(&shared);
let fresh = fabrics(&view);
let confirmed: HashSet<&String> = view.ok_pairs.iter().flat_map(|(a, b)| [a, b]).collect();
for (node, addrs) in &view.rdma {
for a in addrs {
if confirmed.contains(a) {
unverified.remove(a);
} else if unverified.insert(a.clone()) {
log(
"fabric_addr_unverified",
&[
("node", node.clone()),
("addr", a.clone()),
(
"why",
"tagged rdma, but no probe over it has succeeded -- \
a cable that is out, or a tag on the wrong interface"
.to_string(),
),
],
);
}
}
}
{
let st = shared.st.lock().unwrap();
if st.fabrics == fresh {
candidate = None;
continue;
}
}
let since = match &candidate {
Some((c, t)) if *c == fresh => *t,
_ => {
candidate = Some((fresh, Instant::now()));
continue;
}
};
if since.elapsed() < hold_down {
continue;
}
let mut st = shared.st.lock().unwrap();
st.fabrics = fresh.clone();
st.emit(
"islands_changed",
json!({ "islands": fresh.islands.iter().map(|i| &i.nodes).collect::<Vec<_>>(),
"tagged_nodes": fresh.tagged.len() }),
);
candidate = None;
crate::daemon::try_place(&mut st, &shared.cv);
shared.cv.notify_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn view(rdma: &[(&str, &[&str])], ok: &[(&str, &str)]) -> FabricView {
FabricView {
rdma: rdma
.iter()
.map(|(n, a)| (n.to_string(), a.iter().map(|s| s.to_string()).collect()))
.collect(),
ok_pairs: ok
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect(),
}
}
#[test]
fn two_cabled_pairs_are_two_islands() {
let v = view(
&[
("n1", &["10.100.0.1"]),
("n2", &["10.100.0.2"]),
("n3", &["10.100.0.3"]),
("n4", &["10.100.0.4"]),
],
&[("10.100.0.1", "10.100.0.2"), ("10.100.0.3", "10.100.0.4")],
);
let got = islands(&v);
assert_eq!(
got.iter().map(|i| i.nodes.clone()).collect::<Vec<_>>(),
vec![vec!["n1", "n2"], vec!["n3", "n4"]],
);
assert_eq!(got[0].addr["n1"], "10.100.0.1");
assert_eq!(got[1].addr["n4"], "10.100.0.4");
}
#[test]
fn a_shared_subnet_is_not_a_fabric() {
let v = view(&[("n1", &["10.100.0.1"]), ("n2", &["10.100.0.2"])], &[]);
assert!(islands(&v).is_empty());
}
#[test]
fn an_untagged_link_makes_no_island() {
let v = view(&[], &[("192.168.1.11", "192.168.1.12")]);
assert!(islands(&v).is_empty());
}
#[test]
fn a_partly_connected_set_is_pruned_to_one_that_is_whole() {
let v = view(
&[
("n1", &["10.0.0.1"]),
("n2", &["10.0.0.2"]),
("n3", &["10.0.0.3"]),
],
&[("10.0.0.1", "10.0.0.2"), ("10.0.0.1", "10.0.0.3")],
);
let got = islands(&v);
assert_eq!(got.len(), 1, "{got:?}");
assert_eq!(got[0].nodes.len(), 2, "{got:?}");
assert!(got[0].nodes.contains(&"n1".to_string()), "{got:?}");
let (x, y) = (&got[0].nodes[0], &got[0].nodes[1]);
assert!(v.linked(&got[0].addr[x], &got[0].addr[y]), "{got:?}");
}
#[test]
fn a_node_whose_ports_split_across_links_is_left_out() {
let v = view(
&[
("n1", &["10.0.0.1", "10.0.1.1"]),
("n2", &["10.0.0.2"]),
("n3", &["10.0.1.3"]),
],
&[
("10.0.0.1", "10.0.0.2"),
("10.0.1.1", "10.0.1.3"),
("10.0.0.2", "10.0.1.3"),
],
);
let got = islands(&v);
assert_eq!(got.len(), 1, "{got:?}");
assert_eq!(got[0].nodes, vec!["n2", "n3"], "{got:?}");
}
#[test]
fn every_member_has_an_address_that_reaches_every_other() {
let v = view(
&[
("n1", &["10.100.0.1"]),
("n2", &["10.100.0.2"]),
("n3", &["10.100.0.3"]),
],
&[
("10.100.0.1", "10.100.0.2"),
("10.100.0.2", "10.100.0.3"),
("10.100.0.1", "10.100.0.3"),
],
);
let got = islands(&v);
assert_eq!(got[0].nodes.len(), 3);
for x in &got[0].nodes {
for y in &got[0].nodes {
if x != y {
assert!(v.linked(&got[0].addr[x], &got[0].addr[y]), "{x} {y}");
}
}
}
}
#[test]
fn tagging_a_node_opts_it_in_before_any_probe_succeeds() {
let f = fabrics(&view(&[("n1", &["10.0.0.1"]), ("n2", &["10.0.0.2"])], &[]));
assert!(f.islands.is_empty());
assert_eq!(f.tagged.len(), 2);
}
#[test]
fn the_island_address_is_the_one_that_answered() {
let v = view(
&[("n1", &["10.0.0.1", "10.0.1.1"]), ("n2", &["10.0.1.2"])],
&[("10.0.1.1", "10.0.1.2")],
);
let got = islands(&v);
assert_eq!(got[0].addr["n1"], "10.0.1.1");
}
}