#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use std::net::{SocketAddr, TcpListener};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use liminal::channel::{ChannelConfig, ChannelHandle, ChannelMode, ChannelSupervisor, Schema};
use liminal_server::cluster::sync::PeerListener;
use liminal_server::cluster::{self, ClusterHandle};
use liminal_server::config::types::ClusterConfig;
use liminal_server::console::records::canonical_bytes;
use liminal_server::console::wall::WallBase;
use liminal_server::console::{ThisDevice, TopologyChange, TopologyModel};
use liminal_server::health::{
ClusterReadiness, ReadinessState, SharedReadinessState, readiness_check,
};
const COOKIE: &str = "srv005-loopback-cookie";
fn eventually(timeout: Duration, mut condition: impl FnMut() -> bool) -> bool {
let deadline = Instant::now() + timeout;
loop {
if condition() {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn free_port() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let addr = listener.local_addr().expect("read ephemeral addr");
drop(listener);
addr
}
struct Node {
supervisor: ChannelSupervisor,
handle: ClusterHandle,
listen_addr: SocketAddr,
readiness: SharedReadinessState,
}
impl Node {
fn start(node_name: &str, listen_addr: SocketAddr, seeds: Vec<SocketAddr>) -> Self {
Self::start_with(node_name, listen_addr, seeds, None)
}
fn start_with(
node_name: &str,
listen_addr: SocketAddr,
seeds: Vec<SocketAddr>,
peers: Option<PeerListener>,
) -> Self {
let resolver = Arc::new(cluster::discovery::ClusterResolver::new());
let supervisor = ChannelSupervisor::with_distribution(
node_name.to_owned(),
1,
COOKIE.to_owned(),
cluster::discovery::as_resolver(Arc::clone(&resolver)),
liminal::channel::ChannelRestartPolicy::default(),
)
.expect("clustered supervisor starts");
let config = ClusterConfig {
node_name: node_name.to_owned(),
listen_address: listen_addr,
seed_nodes: seeds,
cookie: COOKIE.to_owned(),
};
let readiness = SharedReadinessState::new(ReadinessState::new(
true,
true,
ClusterReadiness::Configured {
membership_established: false,
},
));
let scheduler = supervisor.scheduler();
let supervisor_for_observer = supervisor.clone();
let readiness_for_hook = readiness.clone();
let handle = cluster::start(
&scheduler,
resolver,
&config,
move |sync| {
if let Some(peers) = peers {
assert!(sync.set_peer_listener(peers), "one peer listener");
}
supervisor_for_observer.install_observer(Arc::new(sync));
},
move || readiness_for_hook.set_cluster_membership_established(true),
)
.expect("cluster starts");
Self {
supervisor,
handle,
listen_addr,
readiness,
}
}
fn readiness_ready(&self) -> bool {
readiness_check(&self.readiness.snapshot()).ready
}
fn channel(&self, name: &str) -> ChannelHandle {
let schema = Schema::new(serde_json::json!({})).expect("empty schema");
ChannelHandle::with_supervisor(
ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral),
self.supervisor.clone(),
)
}
fn peer_count(&self) -> usize {
self.handle.membership().peers().len()
}
const fn membership(&self) -> &liminal_server::cluster::Membership {
self.handle.membership()
}
fn shutdown(&self) {
self.supervisor.shutdown();
}
}
fn assert_dialer_knows_its_seeds(node: &Node, expected: usize) {
assert_eq!(
node.peer_count(),
expected,
"a node that dialed its seeds must know them the instant cluster start \
returns — the initial view is atomic, not sampled"
);
}
fn await_inbound_membership(node: &Node, expected: usize) {
assert!(
eventually(Duration::from_secs(10), || node.peer_count() >= expected),
"the accepting node should observe the inbound link as a peer"
);
}
fn await_mutual_membership(a: &Node, b: &Node) {
assert_dialer_knows_its_seeds(b, 1);
await_inbound_membership(a, 1);
}
#[test]
fn cluster_start_flips_node_readiness_to_established() {
let addr_a = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
assert!(
node_a.readiness_ready(),
"a successful zero-seed cluster start must mark the node ready"
);
let addr_b = free_port();
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
assert!(node_a.readiness_ready(), "node A stays ready");
assert!(
node_b.readiness_ready(),
"node B reaches ready after its own successful start"
);
}
#[test]
fn subscription_on_a_is_visible_as_remote_member_on_b() {
let addr_a = free_port();
let addr_b = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
let channel_a = node_a.channel("orders");
let _subscription = channel_a.subscribe().expect("subscribe on A");
let channel_b = node_b.channel("orders");
assert!(
eventually(Duration::from_secs(10), || {
!node_b_remote_members(&node_b, "orders").is_empty()
}),
"node B should see A's subscription as a remote pg member"
);
let members = node_b_remote_members(&node_b, "orders");
assert_eq!(members.len(), 1, "exactly one remote subscriber");
drop(channel_b);
}
#[test]
fn publish_on_b_reaches_subscriber_on_a() {
let addr_a = free_port();
let addr_b = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
let channel_a = node_a.channel("orders");
let subscription = channel_a.subscribe().expect("subscribe on A");
assert!(
eventually(Duration::from_secs(10), || {
!node_b_remote_members(&node_b, "orders").is_empty()
}),
"B should learn A's subscription before publishing"
);
let channel_b = node_b.channel("orders");
let payload = br#"{"order":"cross-node-1"}"#.to_vec();
channel_b.publish(&payload).expect("publish on B");
let received = eventually(Duration::from_secs(10), || {
matches!(subscription.try_next(), Ok(Some(_)))
|| a_inbox_has_payload(&subscription, &payload)
});
assert!(received, "A's subscriber should receive B's publish");
}
#[test]
fn late_joiner_is_backfilled_with_existing_subscriptions() {
let addr_a = free_port();
let addr_c = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
let channel_a = node_a.channel("events");
let _subscription = channel_a.subscribe().expect("subscribe on A");
let node_c = Node::start("node-c@127.0.0.1", addr_c, vec![addr_a]);
assert_dialer_knows_its_seeds(&node_c, 1);
await_inbound_membership(&node_a, 1);
assert!(
eventually(Duration::from_secs(10), || {
!node_b_remote_members(&node_c, "events").is_empty()
}),
"late joiner C should be backfilled with A's existing subscription"
);
}
#[test]
fn dropping_a_node_purges_its_remote_members_and_survivors_still_deliver() {
let addr_a = free_port();
let addr_b = free_port();
let addr_c = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
let node_c = Node::start("node-c@127.0.0.1", addr_c, vec![addr_a, addr_b]);
assert_dialer_knows_its_seeds(&node_c, 2);
await_inbound_membership(&node_a, 2);
await_inbound_membership(&node_b, 2);
let channel_a = node_a.channel("orders");
let sub_a = channel_a.subscribe().expect("subscribe on A");
let channel_c = node_c.channel("orders");
let sub_c = channel_c.subscribe().expect("subscribe on C");
assert!(
eventually(Duration::from_secs(10), || {
node_b_remote_members(&node_b, "orders").len() >= 2
}),
"B should see remote members from both A and C"
);
drop(sub_a);
drop(channel_a);
node_a.shutdown();
drop(node_a);
assert!(
eventually(Duration::from_secs(15), || {
node_b_remote_members(&node_b, "orders").len() == 1
}),
"B should purge A's remote member after A drops, leaving only C"
);
let channel_b = node_b.channel("orders");
let payload = br#"{"order":"after-a-dropped"}"#.to_vec();
channel_b
.publish(&payload)
.expect("publish on B after A dropped");
let received = eventually(Duration::from_secs(10), || {
a_inbox_has_payload(&sub_c, &payload)
});
assert!(
received,
"C's surviving subscriber should still receive B's publish after A dropped"
);
}
#[test]
fn stable_membership_source_has_zero_consumer_wakes() {
let addr_a = free_port();
let addr_b = free_port();
let node_a = Node::start("node-a@127.0.0.1", addr_a, vec![]);
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
let channel_a = node_a.channel("soak");
let subscription = channel_a.subscribe().expect("subscribe on A");
assert!(
eventually(Duration::from_secs(10), || {
!node_b_remote_members(&node_b, "soak").is_empty()
}),
"B should learn A's subscription before the soak begins"
);
let membership = node_a.membership();
assert!(
eventually(Duration::from_secs(10), || membership.pending_effects()
== 0),
"the membership consumer should have applied bring-up before the soak"
);
let wakes_at_rest = membership.consumer_wakes();
let snapshots_at_rest = membership.source_snapshots();
let events_at_rest = membership.events_observed();
let channel_b = node_b.channel("soak");
let deadline = Instant::now() + Duration::from_secs(3);
let mut published = 0_u64;
let mut delivered = 0_u64;
while Instant::now() < deadline {
let payload = format!(r#"{{"soak":{published}}}"#).into_bytes();
channel_b
.publish(&payload)
.expect("publish on B during the soak");
published += 1;
while let Ok(Some(_)) = subscription.try_next() {
delivered += 1;
}
std::thread::sleep(Duration::from_millis(10));
}
while let Ok(Some(_)) = subscription.try_next() {
delivered += 1;
}
assert!(
published >= 50,
"the soak must actually drive the cluster (published {published})"
);
assert!(
delivered > 0,
"cross-node delivery must GROW across the soak, or the flat membership \
counters below prove only that the test slept (published {published}, \
delivered {delivered})"
);
assert_eq!(
membership.consumer_wakes(),
wakes_at_rest,
"a stable membership source must wake its consumer zero times"
);
assert_eq!(
membership.source_snapshots(),
snapshots_at_rest,
"the initial view is acquired once at arm and never again — no cadence"
);
assert_eq!(
membership.events_observed(),
events_at_rest,
"no membership traffic means no membership events"
);
assert_eq!(
membership.pending_effects(),
0,
"nothing queued, because nothing happened"
);
assert_eq!(
node_a.peer_count(),
1,
"and the view is still correct at the end of the soak"
);
}
fn node_b_remote_members(node: &Node, channel: &str) -> Vec<beamr::distribution::pg::RemoteMember> {
let scheduler = node.supervisor.scheduler();
let atoms = scheduler.atom_table();
let pg = scheduler.pg_registry();
let group = atoms.intern(channel);
pg.remote_members(pg.default_scope(), group)
}
fn a_inbox_has_payload(
subscription: &liminal::channel::SubscriptionHandle,
payload: &[u8],
) -> bool {
while let Ok(Some(envelope)) = subscription.try_next() {
if envelope.payload == payload {
return true;
}
}
false
}
impl std::fmt::Debug for Node {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Node")
.field("listen_addr", &self.listen_addr)
.finish_non_exhaustive()
}
}
enum Seen {
Leaving,
Record(serde_json::Value),
Failed(String),
}
#[test]
fn membership_join_and_leave_is_two_records() -> Result<(), Box<dyn std::error::Error>> {
let local = "node-a@127.0.0.1";
let model = Arc::new(Mutex::new(TopologyModel::new(
ThisDevice {
id: local.to_owned(),
liminal: env!("CARGO_PKG_VERSION").to_owned(),
started: Instant::now(),
depot: false,
},
[],
[],
[],
[],
)));
let (seen, sightings) = std::sync::mpsc::channel::<Seen>();
let (open_gate, gate) = std::sync::mpsc::channel::<()>();
let gate = Mutex::new(gate);
let listener: PeerListener = {
let model = Arc::clone(&model);
Arc::new(move |name, present| {
let built = (|| -> Result<Option<serde_json::Value>, String> {
if !present {
seen.send(Seen::Leaving)
.map_err(|error| error.to_string())?;
gate.lock()
.map_err(|error| error.to_string())?
.recv()
.map_err(|error| error.to_string())?;
}
let change = TopologyChange::Peer {
name: name.to_owned(),
present,
};
let mut model = model.lock().map_err(|error| error.to_string())?;
let wall = WallBase::now().map_err(|error| error.to_string())?;
if !model.apply(change) {
return Ok(None);
}
let (document, fault) = model.document(&wall);
if let Some(error) = fault {
return Err(error.to_string());
}
drop(model);
let bytes =
canonical_bytes(&document, "topology").map_err(|error| error.to_string())?;
serde_json::from_slice(&bytes)
.map(Some)
.map_err(|error| error.to_string())
})();
let _ = match built {
Ok(Some(record)) => seen.send(Seen::Record(record)),
Ok(None) => Ok(()),
Err(error) => seen.send(Seen::Failed(error)),
};
})
};
let next = || -> Result<Seen, Box<dyn std::error::Error>> {
Ok(sightings.recv_timeout(Duration::from_secs(15))?)
};
let addr_a = free_port();
let addr_b = free_port();
let node_a = Node::start_with(local, addr_a, vec![], Some(listener));
let node_b = Node::start("node-b@127.0.0.1", addr_b, vec![addr_a]);
await_mutual_membership(&node_a, &node_b);
let mut records = Vec::new();
match next()? {
Seen::Record(record) => records.push(record),
Seen::Leaving => return Err("a leave before the join".into()),
Seen::Failed(error) => return Err(error.into()),
}
node_b.shutdown();
drop(node_b);
match next()? {
Seen::Leaving => {}
Seen::Record(record) => return Err(format!("a record before the leave: {record}").into()),
Seen::Failed(error) => return Err(error.into()),
}
assert_eq!(
node_a.peer_count(),
0,
"the count moves before the listener runs"
);
assert_eq!(
records.len(),
1,
"at the count, the leave is not yet recorded"
);
open_gate.send(())?;
match next()? {
Seen::Record(record) => records.push(record),
Seen::Leaving => return Err("a second leave".into()),
Seen::Failed(error) => return Err(error.into()),
}
join_then_leave(&records, local)
}
fn join_then_leave(
records: &[serde_json::Value],
local: &str,
) -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(records.len(), 2, "{records:?}");
let devices = |record: &serde_json::Value| -> Result<Vec<String>, String> {
record["devices"]
.as_array()
.ok_or_else(|| format!("no devices: {record}"))?
.iter()
.map(|device| {
device["id"]
.as_str()
.map(str::to_owned)
.ok_or_else(|| format!("a device with no id: {device}"))
})
.collect()
};
let joined = &records[0];
let peer = devices(joined)?
.into_iter()
.find(|id| id != local)
.ok_or("the peer is not a device")?;
assert!(peer.contains("node-b"), "{peer}");
let links = joined["links"].as_array().ok_or("no links")?;
assert_eq!(links.len(), 1, "{joined}");
assert_eq!(links[0]["state"], "up");
assert!(
links[0]["ends"]
.as_array()
.ok_or("a link with no ends")?
.iter()
.any(|end| end.as_str() == Some(peer.as_str())),
"{joined}"
);
let left = &records[1];
assert_eq!(devices(left)?, vec![local.to_owned()]);
assert_eq!(left["links"].as_array().map(Vec::len), Some(0));
Ok(())
}