mod support;
use spate_coordination::{SplitCoordinator, SplitProgress};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use support::{Held, PhasedPlanner, drive, runtime, store, worker};
#[derive(Clone)]
struct Capture(Arc<Mutex<Vec<u8>>>);
impl Capture {
fn new() -> Capture {
Capture(Arc::new(Mutex::new(Vec::new())))
}
fn lines(&self) -> Vec<String> {
String::from_utf8_lossy(&self.0.lock().expect("capture"))
.lines()
.map(str::to_string)
.collect()
}
}
impl std::io::Write for Capture {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().expect("capture").extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for Capture {
type Writer = Capture;
fn make_writer(&'a self) -> Capture {
self.clone()
}
}
fn announcements(capture: &Capture) -> usize {
capture
.lines()
.iter()
.filter(|l| l.contains("assignment published"))
.count()
}
fn announced_moves(capture: &Capture) -> Vec<u64> {
capture
.lines()
.iter()
.filter(|l| l.contains("assignment published"))
.map(|l| {
l.split_whitespace()
.find_map(|f| f.strip_prefix("moved="))
.unwrap_or_else(|| panic!("no `moved` field to read on: {l}"))
.parse()
.expect("moved is a count")
})
.collect()
}
fn wait_for_line(capture: &Capture, needle: &str) {
wait_until(&format!("a line containing {needle:?}"), capture, |c| {
c.lines().iter().any(|l| l.contains(needle))
});
}
fn wait_until(what: &str, capture: &Capture, mut check: impl FnMut(&Capture) -> bool) {
let deadline = Instant::now() + support::DEADLINE;
while Instant::now() < deadline {
if check(capture) {
return;
}
std::thread::sleep(support::POLL_INTERVAL);
}
panic!(
"timed out waiting for {what}\n--- captured ---\n{}",
capture.lines().join("\n")
);
}
#[test]
fn a_peer_joining_is_announced_and_nothing_reads_as_a_fault() {
let capture = Capture::new();
tracing_subscriber::fmt()
.with_writer(capture.clone())
.with_max_level(tracing::Level::INFO)
.without_time()
.init();
let rt = runtime();
let store = store();
let ids = ["s0", "s1", "s2", "s3"];
let planner = || Box::new(PhasedPlanner::one_final("joinlog:v1", &ids));
let mut a = worker(&store, rt.handle(), Some("worker-a"));
a.start(planner()).unwrap();
let mut held_a = Held::default();
drive(&mut a, &mut held_a, "worker-a takes the whole plan", |h| {
h.splits.len() == ids.len()
});
support::commit_held(&mut a, &held_a);
let announced_alone = announcements(&capture);
for id in ["s0", "s1"] {
let done = SplitProgress::completed(support::DRAINED_WATERMARK, vec![]);
a.commit(&support::split_id(id), &done).expect("commit");
held_a.splits.remove(id);
}
let settle_until = Instant::now() + support::LEASE;
while Instant::now() < settle_until {
held_a.fold(a.poll().unwrap());
std::thread::sleep(support::POLL_INTERVAL);
}
assert_eq!(held_a.splits.len(), 2, "two splits are left to hold");
assert_eq!(
announcements(&capture),
announced_alone,
"a completion was announced as a rebalance\n--- captured ---\n{}",
capture.lines().join("\n")
);
let mut b = worker(&store, rt.handle(), Some("worker-b"));
b.start(planner()).unwrap();
let mut held_b = Held::default();
let deadline = Instant::now() + support::DEADLINE;
while !(held_a.splits.len() == 1 && held_b.splits.len() == 1) {
assert!(
Instant::now() < deadline,
"timed out balancing: a={:?} b={:?}",
held_a.splits.keys().collect::<Vec<_>>(),
held_b.splits.keys().collect::<Vec<_>>()
);
held_a.fold(a.poll().unwrap());
held_b.fold(b.poll().unwrap());
support::commit_held(&mut a, &held_a);
support::consent_to_revocations(&mut a, &mut held_a);
std::thread::sleep(support::POLL_INTERVAL);
}
wait_for_line(&capture, "peer joined");
wait_until(
"the join is announced as a rebalance of its own",
&capture,
|c| announcements(c) > announced_alone,
);
wait_for_line(&capture, "joined a fleet already running");
let lines = capture.lines();
let joins: Vec<&String> = lines.iter().filter(|l| l.contains("peer joined")).collect();
assert!(
joins.iter().any(|l| l.contains("worker-b")),
"no join named worker-b\n--- captured ---\n{}",
lines.join("\n")
);
let moves = announced_moves(&capture);
assert!(
moves[announced_alone..].iter().any(|m| *m > 0),
"worker-b took a split off worker-a, so the publish that followed \
the join moved it\n--- captured ---\n{}",
lines.join("\n")
);
let alarming: Vec<&String> = lines
.iter()
.filter(|l| l.contains("deleted externally") || l.contains("_probe"))
.collect();
assert!(
alarming.is_empty(),
"a routine join logged {} line(s) that read as a fault:\n{}",
alarming.len(),
alarming
.iter()
.map(|l| l.as_str())
.collect::<Vec<_>>()
.join("\n")
);
let before_departure = announcements(&capture);
let leaving: Vec<_> = held_b
.splits
.keys()
.map(|id| support::split_id(id))
.collect();
b.release(&leaving).expect("worker-b departs");
drop(b);
drive(
&mut a,
&mut held_a,
"worker-a inherits the departed share",
|h| h.splits.len() == 2,
);
wait_for_line(&capture, "peer left");
let moves = announced_moves(&capture);
assert!(
moves[before_departure..].iter().any(|m| *m > 0),
"worker-a inherited worker-b's split, so the publish that followed \
the departure moved it\n--- captured ---\n{}",
capture.lines().join("\n")
);
}