#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::fmt::Write as _;
use std::sync::Arc;
use rings_transport::connections::dummy_controlled;
use crate::dht::Chord;
use crate::dht::PeerRing;
use crate::ecc::SecretKey;
use crate::inspect::DHTInspect;
use crate::session::SessionSk;
use crate::storage::MemStorage;
use crate::swarm::Swarm;
use crate::swarm::SwarmBuilder;
use crate::tests::default::Node;
use crate::tests::manually_establish_connection;
const KEYS: [&str; 6] = [
"9c83fcb684af3dc71018b5a303245d2f2fed8a579096589f3234a67a52a7ac66",
"fd674cb6089663935cb061254602e343da8a2fa3908980ae4f7a27adb8b7ac8a",
"b9ce7159a2ad3b9fe885a7744d32afeec233e7ddeaed0759cbab2c00a1bd548b",
"4efb629f54a3f3dd91f5efffc4f9b51ab27eb082b2393067757681ed6439480d",
"f2cbca82fb82745c1f9d94c1c9d2b0606daaf6f15ac8a215fc72c8bc0478ecf5",
"e1d7f24e2b725df077627fc0337b9c53b37ce594ca84fccd0f36dc58423a0ed2",
];
const SCHEDULE_FINGER_TABLE_SIZE: usize = 16;
const MAX_ROUNDS: usize = 120;
const MAX_DELIVERIES: usize = 2_000_000;
const MAX_DELIVERIES_PER_ROUND: usize = 100_000;
const MAX_STAGNANT_ROUNDS: usize = 20;
const TRACE_WINDOW: usize = 16;
struct ControlledDeliveryGuard;
impl ControlledDeliveryGuard {
fn enable() -> Self {
dummy_controlled::enable(true);
Self
}
}
impl Drop for ControlledDeliveryGuard {
fn drop(&mut self) {
dummy_controlled::enable(false);
}
}
#[derive(Debug, Default)]
struct DrainStats {
deliveries: usize,
dropped: usize,
max_pending: usize,
}
#[derive(Debug)]
struct RoundTrace {
phase: String,
deliveries: usize,
dropped: usize,
total_delivered: usize,
max_pending: usize,
changed: bool,
topology_matches: usize,
full_matches: usize,
finger_known_slots: Vec<u64>,
first_mismatch: String,
}
#[derive(Debug, Default)]
struct ScheduleDiagnostics {
traces: VecDeque<RoundTrace>,
}
impl ScheduleDiagnostics {
fn push(&mut self, trace: RoundTrace) {
if self.traces.len() == TRACE_WINDOW {
self.traces.pop_front();
}
self.traces.push_back(trace);
}
fn render(&self, actual: &[DHTInspect], expected: &[DHTInspect]) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"finger_table_size={SCHEDULE_FINGER_TABLE_SIZE}, max_rounds={MAX_ROUNDS}, \
max_deliveries={MAX_DELIVERIES}, max_deliveries_per_round={MAX_DELIVERIES_PER_ROUND}"
);
let _ = writeln!(out, "recent rounds:");
for trace in &self.traces {
let _ = writeln!(
out,
" {phase}: delivered={delivered} dropped={dropped} total={total} \
max_pending={max_pending} changed={changed} topology={topology}/{nodes} \
full={full}/{nodes} finger_known_slots={finger:?} first_mismatch={mismatch}",
phase = trace.phase,
delivered = trace.deliveries,
dropped = trace.dropped,
total = trace.total_delivered,
max_pending = trace.max_pending,
changed = trace.changed,
topology = trace.topology_matches,
nodes = expected.len(),
full = trace.full_matches,
finger = trace.finger_known_slots,
mismatch = trace.first_mismatch,
);
}
let _ = writeln!(out, "current first mismatch:");
let _ = write!(out, "{}", describe_first_mismatch(actual, expected));
let _ = writeln!(out, "current topology mismatches:");
let _ = write!(out, "{}", describe_topology_mismatches(actual, expected));
out
}
}
fn inspect_all(swarms: &[Arc<Swarm>]) -> Vec<DHTInspect> {
swarms
.iter()
.map(|swarm| DHTInspect::inspect(&swarm.dht()))
.collect()
}
fn topology_matches(actual: &DHTInspect, expected: &DHTInspect) -> bool {
actual.successors == expected.successors && actual.predecessor == expected.predecessor
}
fn known_finger_slots(dht: &DHTInspect) -> u64 {
dht.finger_table
.iter()
.filter(|(did, _, _)| did.is_some())
.map(|(_, start, end)| end - start + 1)
.sum()
}
fn short_did(did: &str) -> &str {
did.get(..10).unwrap_or(did)
}
fn mismatch_summary(actual: &[DHTInspect], expected: &[DHTInspect]) -> String {
actual
.iter()
.zip(expected)
.enumerate()
.find(|(_, (act, exp))| act != exp)
.map(|(i, (act, exp))| {
let topic = if !topology_matches(act, exp) {
"topology"
} else {
"finger"
};
format!("node{i}/{}:{topic}", short_did(&act.did))
})
.unwrap_or_else(|| "none".to_string())
}
fn describe_first_mismatch(actual: &[DHTInspect], expected: &[DHTInspect]) -> String {
for (i, (act, exp)) in actual.iter().zip(expected).enumerate() {
if act == exp {
continue;
}
return format!(
"node{i} {}\nactual: successors={:?} predecessor={:?} finger={:?}\n\
expected: successors={:?} predecessor={:?} finger={:?}\n",
act.did,
act.successors,
act.predecessor,
act.finger_table,
exp.successors,
exp.predecessor,
exp.finger_table,
);
}
"none\n".to_string()
}
fn describe_topology_mismatches(actual: &[DHTInspect], expected: &[DHTInspect]) -> String {
let mut out = String::new();
for (i, (act, exp)) in actual.iter().zip(expected).enumerate() {
if topology_matches(act, exp) {
continue;
}
let _ = writeln!(
out,
"node{i} {} actual_succ={:?} actual_pred={:?} expected_succ={:?} expected_pred={:?}",
short_did(&act.did),
act.successors,
act.predecessor,
exp.successors,
exp.predecessor,
);
}
if out.is_empty() {
out.push_str("none\n");
}
out
}
fn round_trace(
phase: impl Into<String>,
stats: DrainStats,
total_delivered: usize,
before: &[DHTInspect],
actual: &[DHTInspect],
expected: &[DHTInspect],
) -> RoundTrace {
RoundTrace {
phase: phase.into(),
deliveries: stats.deliveries,
dropped: stats.dropped,
total_delivered,
max_pending: stats.max_pending,
changed: before != actual,
topology_matches: actual
.iter()
.zip(expected)
.filter(|(act, exp)| topology_matches(act, exp))
.count(),
full_matches: actual
.iter()
.zip(expected)
.filter(|(act, exp)| act == exp)
.count(),
finger_known_slots: actual.iter().map(known_finger_slots).collect(),
first_mismatch: mismatch_summary(actual, expected),
}
}
async fn prepare_schedule_node(key: SecretKey) -> Node {
let stun = "stun://stun.l.google.com:19302";
let storage = Box::new(MemStorage::new());
let session_sk = SessionSk::new_with_seckey(&key).unwrap();
let swarm = Arc::new(
SwarmBuilder::new(0, stun, storage, session_sk)
.dht_finger_table_size(SCHEDULE_FINGER_TABLE_SIZE)
.build(),
);
println!("key: {:?}", key.to_string());
println!("did: {:?}", swarm.did());
Node::new(swarm)
}
fn gen_schedule_dht(did: crate::dht::Did) -> PeerRing {
let storage = Box::new(MemStorage::new());
PeerRing::new_with_storage_and_finger_table_size(
did,
3,
storage,
SCHEDULE_FINGER_TABLE_SIZE,
)
}
fn expected_dhts(swarms: &[Arc<Swarm>]) -> Vec<DHTInspect> {
swarms
.iter()
.map(|swarm| {
let dht = gen_schedule_dht(swarm.did());
for other in swarms {
if dht.did != other.did() {
dht.join(other.did()).unwrap();
dht.notify(other.did()).unwrap();
}
}
DHTInspect::inspect(&dht)
})
.collect()
}
async fn drain(pick: fn(usize) -> usize, delivered: &mut usize) -> DrainStats {
let mut stats = DrainStats::default();
while dummy_controlled::pending() > 0 {
let pending = dummy_controlled::pending();
stats.max_pending = stats.max_pending.max(pending);
let idx = pick(pending);
if !dummy_controlled::deliver(idx).await {
stats.dropped += 1;
}
*delivered += 1;
stats.deliveries += 1;
assert!(
*delivered < MAX_DELIVERIES,
"runaway delivery - likely a routing self-route loop"
);
assert!(
stats.deliveries < MAX_DELIVERIES_PER_ROUND,
"round delivery budget exceeded: delivered {} in one drain, max_pending {}",
stats.deliveries,
stats.max_pending
);
}
stats
}
async fn run_schedule(pick: fn(usize) -> usize) {
let mut nodes = vec![];
for k in KEYS {
nodes.push(prepare_schedule_node(SecretKey::try_from(k).unwrap()).await);
}
let swarms: Vec<Arc<Swarm>> = nodes.iter().map(|n| n.swarm.clone()).collect();
let expected = expected_dhts(&swarms);
let mut diagnostics = ScheduleDiagnostics::default();
let _controlled = ControlledDeliveryGuard::enable();
for sw in swarms.iter().skip(1) {
manually_establish_connection(&swarms[0], sw).await;
}
let mut delivered = 0usize;
let before_bootstrap = inspect_all(&swarms);
let bootstrap_stats = drain(pick, &mut delivered).await;
let mut actual = inspect_all(&swarms);
diagnostics.push(round_trace(
"bootstrap",
bootstrap_stats,
delivered,
&before_bootstrap,
&actual,
&expected,
));
let mut ok = false;
let mut stagnant_rounds = 0usize;
for round in 1..=MAX_ROUNDS {
let before = actual.clone();
for sw in &swarms {
let _ = sw.stabilizer().stabilize().await;
}
let stats = drain(pick, &mut delivered).await;
actual = inspect_all(&swarms);
let changed = before != actual;
diagnostics.push(round_trace(
format!("round{round}"),
stats,
delivered,
&before,
&actual,
&expected,
));
if actual == expected {
ok = true;
break;
}
if changed {
stagnant_rounds = 0;
} else {
stagnant_rounds += 1;
assert!(
stagnant_rounds < MAX_STAGNANT_ROUNDS,
"schedule made no DHT progress for {stagnant_rounds} rounds\n{}",
diagnostics.render(&actual, &expected)
);
}
}
assert!(
ok,
"did not converge under the chosen delivery schedule\n{}",
diagnostics.render(&actual, &expected)
);
for (i, (act, exp)) in actual.iter().zip(&expected).enumerate() {
pretty_assertions::assert_eq!(act, exp, "node{i}");
}
drop(nodes);
}
fn fifo(_pending: usize) -> usize {
0
}
fn lifo(pending: usize) -> usize {
pending - 1
}
#[tokio::test]
async fn schedule_fifo_converges() {
run_schedule(fifo).await;
}
#[tokio::test]
async fn schedule_lifo_converges() {
run_schedule(lifo).await;
}
}