use std::cell::{Cell, RefCell};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use serde_json::Value;
use super::error::ConsoleError;
use super::hub::{ConsoleHub, MAX_LIVE_TAPS, PumpReport, TOPOLOGY_SUBJECT};
use super::model::{FlowRole, ThisDevice, TopologyChange, TopologyModel};
use super::publisher::ServerPublisher;
use super::records::{LineLife, LineState};
use super::wall::WallBase;
use crate::tap::{PublishFact, TapOutcome, TapSource};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const ORDERS_TAP: &str = "console.tap.orders";
fn hub(capacity: usize) -> Result<ConsoleHub, ConsoleError> {
let started = Instant::now();
let model = TopologyModel::new(
ThisDevice {
id: "local".to_owned(),
liminal: "1.2.3".to_owned(),
started,
depot: false,
},
[],
[],
[],
[],
);
ConsoleHub::with_capacity("local", model, WallBase::fixed(1_000, started), capacity)
}
fn publish(hub: &ConsoleHub, times: usize) -> TestResult {
for _ in 0..times {
hub.tap().observe(&PublishFact {
subject: "orders",
payload: b"{}",
outcome: TapOutcome::Delivered,
message_id: None,
source: TapSource::Connection(7),
reply_to: None,
recorded: None,
})?;
}
Ok(())
}
#[derive(Default)]
struct Scripted {
failures: Cell<u32>,
records: RefCell<Vec<(String, Value)>>,
retired: RefCell<Vec<String>>,
}
impl Scripted {
fn take(&self) -> Vec<(String, Value)> {
self.records.take()
}
}
fn refused(subject: &str) -> ConsoleError {
ConsoleError::Server {
subject: subject.to_owned(),
source: crate::ServerError::ListenerAccept {
message: "scripted refusal".to_owned(),
},
}
}
impl ServerPublisher for Scripted {
fn publish_console(&self, subject: &str, payload: &[u8]) -> Result<(), ConsoleError> {
if self.failures.get() > 0 {
self.failures.set(self.failures.get() - 1);
return Err(refused(subject));
}
let value = serde_json::from_slice(payload).map_err(|source| ConsoleError::Encode {
record: "test read-back",
source,
})?;
self.records.borrow_mut().push((subject.to_owned(), value));
Ok(())
}
fn retire_console_channel(&self, subject: &str) -> Result<(), ConsoleError> {
self.retired.borrow_mut().push(subject.to_owned());
Ok(())
}
}
fn shape(records: &[(String, Value)]) -> Result<Vec<(u64, u64, u64, usize)>, String> {
records
.iter()
.map(|(_, record)| {
let number = |field: &str| {
record[field]
.as_u64()
.ok_or_else(|| format!("tap record has no numeric {field}"))
};
let messages = record["messages"]
.as_array()
.ok_or("tap record has no messages")?;
Ok((
number("seq")?,
number("sampled")?,
number("dropped")?,
messages.len(),
))
})
.collect()
}
#[test]
fn a_failed_tap_publication_is_retried_first() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
hub.attach(ORDERS_TAP)?;
publish(&hub, 3)?;
out.failures.set(1);
let failed = hub.pump(&out)?;
assert_eq!(
failed,
PumpReport {
publish_failures: 1,
..PumpReport::default()
}
);
assert_eq!(hub.counts().console_pending_records, 1);
publish(&hub, 2)?;
assert_eq!(hub.pump(&out)?.tap_records, 1);
assert_eq!(
shape(&out.take())?,
vec![(1, 1, 0, 3)],
"the kept record first"
);
assert_eq!(hub.pump(&out)?.tap_records, 1);
let drained = out.take();
assert_eq!(drained[0].0, ORDERS_TAP);
assert_eq!(shape(&drained)?, vec![(2, 1, 0, 2)]);
let counts = hub.counts();
assert_eq!(counts.console_publish_failures, 1);
assert_eq!(counts.tap_records_published, 2);
assert_eq!(counts.console_pending_records, 0);
assert_eq!(counts.console_records_lost, 0);
Ok(())
}
#[test]
fn a_second_failure_loses_only_to_eviction() -> TestResult {
let hub = hub(2)?;
let out = Scripted::default();
hub.attach(ORDERS_TAP)?;
publish(&hub, 1)?;
out.failures.set(2);
assert_eq!(hub.pump(&out)?.publish_failures, 1);
publish(&hub, 8)?;
assert_eq!(hub.pump(&out)?.publish_failures, 1);
assert_eq!(
hub.counts().console_pending_records,
1,
"seq 1 is still kept"
);
assert_eq!(hub.pump(&out)?.tap_records, 1);
assert_eq!(hub.pump(&out)?.tap_records, 2);
assert_eq!(
shape(&out.take())?,
vec![(1, 1, 0, 1), (2, 2, 2, 1), (3, 4, 0, 1)]
);
let counts = hub.counts();
assert_eq!(counts.tap_dropped_total, 2);
assert_eq!(counts.console_records_lost, 0, "the ring is the only loss");
assert_eq!(counts.console_publish_failures, 2);
Ok(())
}
#[test]
fn a_new_topology_subscriber_gets_the_latest_document() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
let wakes = Arc::new(AtomicU64::new(0));
let counter = Arc::clone(&wakes);
hub.set_notifier(Arc::new(move || {
counter.fetch_add(1, Ordering::Relaxed);
}))?;
let opened = TopologyChange::Service {
subject: "orders.reply".to_owned(),
present: true,
};
hub.emit(opened.clone())?;
assert_eq!(wakes.load(Ordering::Relaxed), 0, "idle with no subscriber");
assert_eq!(hub.pump(&out)?, PumpReport::default());
hub.attach(TOPOLOGY_SUBJECT)?;
assert_eq!(wakes.load(Ordering::Relaxed), 1);
assert_eq!(hub.pump(&out)?.topology_records, 1);
hub.attach(TOPOLOGY_SUBJECT)?;
assert_eq!(hub.pump(&out)?.topology_records, 1);
let sent = out.take();
let revs: Vec<_> = sent.iter().map(|(_, doc)| doc["rev"].as_u64()).collect();
assert_eq!(revs, vec![Some(2), Some(2)], "start is 1; one change");
let stations: Vec<&str> = sent[0].1["stations"]
.as_array()
.ok_or("no stations")?
.iter()
.filter_map(|station| station["id"].as_str())
.collect();
assert_eq!(
stations,
["local", "orders.reply"],
"the exchange and the service"
);
hub.emit(opened)?;
assert_eq!(
hub.pump(&out)?,
PumpReport::default(),
"a repeat is no change"
);
assert_eq!(hub.counts().topology_records_published, 2);
Ok(())
}
#[test]
fn a_failed_topology_publication_is_sent_as_it_stands_next() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
hub.attach(TOPOLOGY_SUBJECT)?;
out.failures.set(1);
assert_eq!(hub.pump(&out)?.publish_failures, 1);
hub.emit(TopologyChange::Peer {
name: "edge".to_owned(),
present: true,
})?;
assert_eq!(hub.pump(&out)?.topology_records, 1);
let sent = out.take();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].1["rev"].as_u64(), Some(2));
assert_eq!(hub.pump(&out)?, PumpReport::default());
Ok(())
}
#[test]
fn a_workers_station_is_named_until_its_connection_closes() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
hub.attach("console.tap.orders")?;
publish(&hub, 1)?;
hub.emit(TopologyChange::Worker {
connection: 7,
identity: "orders.pricer".to_owned(),
})?;
publish(&hub, 1)?;
hub.emit(TopologyChange::ConnectionClosed { id: 7 })?;
publish(&hub, 1)?;
hub.pump(&out)?;
let stations: Vec<Option<Value>> = out
.take()
.iter()
.filter(|(subject, _)| subject == "console.tap.orders")
.flat_map(|(_, record)| record["messages"].as_array().cloned().unwrap_or_default())
.map(|message| message.get("from_station").cloned())
.collect();
assert_eq!(stations, [None, Some(Value::from("orders.pricer")), None]);
Ok(())
}
#[test]
fn the_live_tap_limit_refuses_one_more() -> TestResult {
let hub = hub(8)?;
for target in 0..MAX_LIVE_TAPS {
hub.attach(&format!("console.tap.t{target}"))?;
}
assert!(!hub.tap_room("console.tap.one-more")?);
assert!(hub.tap_room("console.tap.t0")?);
assert!(hub.tap_room("orders")?);
assert!(matches!(
hub.attach("console.tap.one-more"),
Err(ConsoleError::TapLimit {
limit: MAX_LIVE_TAPS,
..
})
));
hub.attach("console.tap.t0")?;
let counts = hub.counts();
assert_eq!(counts.tap_attach_failures, 1);
assert_eq!(counts.taps_attached, 256);
Ok(())
}
#[test]
fn the_last_subscriber_retires_the_tap_channel() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
hub.attach(ORDERS_TAP)?;
hub.attach(ORDERS_TAP)?;
hub.detach(ORDERS_TAP)?;
assert_eq!(hub.pump(&out)?.retired, 0);
assert_eq!(hub.attached_subjects()?, vec![ORDERS_TAP.to_owned()]);
hub.detach(ORDERS_TAP)?;
assert!(hub.attached_subjects()?.is_empty());
assert_eq!(hub.tap().subscriber_count(), 0);
assert_eq!(hub.pump(&out)?.retired, 1);
assert_eq!(out.retired.take(), vec![ORDERS_TAP.to_owned()]);
hub.attach(ORDERS_TAP)?;
hub.detach(ORDERS_TAP)?;
hub.attach(ORDERS_TAP)?;
assert_eq!(hub.pump(&out)?.retired, 0, "attached again: it stays");
assert_eq!(hub.counts().taps_attached, 1);
Ok(())
}
struct LockWitness<'hub> {
hub: &'hub ConsoleHub,
held: RefCell<Vec<bool>>,
}
impl ServerPublisher for LockWitness<'_> {
fn publish_console(&self, _subject: &str, _payload: &[u8]) -> Result<(), ConsoleError> {
Ok(())
}
fn retire_console_channel(&self, _subject: &str) -> Result<(), ConsoleError> {
self.held.borrow_mut().push(self.hub.taps_held());
Ok(())
}
}
#[test]
fn a_tap_channel_leaves_the_roster_under_the_taps_lock() -> TestResult {
let hub = hub(8)?;
hub.attach(ORDERS_TAP)?;
hub.detach(ORDERS_TAP)?;
let out = LockWitness {
hub: &hub,
held: RefCell::new(Vec::new()),
};
assert_eq!(hub.pump(&out)?.retired, 1);
assert_eq!(out.held.take(), vec![true]);
Ok(())
}
#[test]
fn a_departed_taps_records_are_counted_as_abandoned() -> TestResult {
let hub = hub(8)?;
let out = Scripted::default();
hub.attach(ORDERS_TAP)?;
publish(&hub, 3)?;
out.failures.set(1);
hub.pump(&out)?;
publish(&hub, 2)?;
hub.detach(ORDERS_TAP)?;
let counts = hub.counts();
assert_eq!(counts.tap_records_abandoned, 1);
assert_eq!(counts.tap_messages_abandoned, 2);
assert_eq!(counts.console_records_lost, 0);
Ok(())
}
struct Resubscribing<'hub> {
hub: &'hub ConsoleHub,
calls: Cell<u64>,
}
impl ServerPublisher for Resubscribing<'_> {
fn publish_console(&self, subject: &str, _payload: &[u8]) -> Result<(), ConsoleError> {
self.calls.set(self.calls.get() + 1);
if self.calls.get() > 1 {
return Ok(());
}
self.hub.detach(subject).map_err(ConsoleError::from)?;
self.hub.attach(subject)?;
Err(refused(subject))
}
fn retire_console_channel(&self, _subject: &str) -> Result<(), ConsoleError> {
Ok(())
}
}
#[test]
fn a_resubscribed_tap_gets_none_of_the_old_taps_records() -> TestResult {
let hub = hub(8)?;
hub.attach(ORDERS_TAP)?;
publish(&hub, 3)?;
let out = Resubscribing {
hub: &hub,
calls: Cell::new(0),
};
assert_eq!(hub.pump(&out)?.publish_failures, 1);
let counts = hub.counts();
assert_eq!(counts.console_pending_records, 0);
assert_eq!(counts.tap_records_abandoned, 1);
Ok(())
}
#[test]
fn a_failed_take_keeps_every_other_taps_records() -> TestResult {
const BILLING_TAP: &str = "console.tap.billing";
let hub = hub(8)?;
let out = Scripted::default();
hub.attach(BILLING_TAP)?;
hub.attach(ORDERS_TAP)?;
hub.tap().observe(&PublishFact {
subject: "billing",
payload: b"{}",
outcome: TapOutcome::Delivered,
message_id: None,
source: TapSource::Connection(7),
reply_to: None,
recorded: None,
})?;
publish(&hub, 2)?;
hub.fail_next_take(ORDERS_TAP);
assert!(hub.pump(&out).is_err(), "the failed take is returned");
let subjects: Vec<String> = out.take().into_iter().map(|(subject, _)| subject).collect();
assert_eq!(subjects, vec![BILLING_TAP.to_owned()]);
assert_eq!(hub.pump(&out)?.tap_records, 1);
assert_eq!(shape(&out.take())?, vec![(1, 1, 0, 2)]);
assert_eq!(hub.counts().console_records_lost, 0);
Ok(())
}
#[test]
fn a_clock_error_in_the_document_never_stops_the_feed() -> TestResult {
let at = Instant::now();
let started = at
.checked_sub(std::time::Duration::from_secs(2))
.ok_or("no instant two seconds back")?;
let model = TopologyModel::new(
ThisDevice {
id: "local".to_owned(),
liminal: "1.2.3".to_owned(),
started,
depot: false,
},
[],
[],
[],
[],
);
let hub = ConsoleHub::with_capacity("local", model, WallBase::fixed(1_000, at), 8)?;
let out = Scripted::default();
hub.attach(TOPOLOGY_SUBJECT)?;
hub.attach(ORDERS_TAP)?;
publish(&hub, 2)?;
hub.pump(&out)?;
publish(&hub, 1)?;
hub.pump(&out)?;
hub.attach(TOPOLOGY_SUBJECT)?;
hub.pump(&out)?;
let records = out.take();
let subjects: Vec<&str> = records
.iter()
.map(|(subject, _)| subject.as_str())
.collect();
assert_eq!(
subjects,
[TOPOLOGY_SUBJECT, ORDERS_TAP, ORDERS_TAP, TOPOLOGY_SUBJECT]
);
for (_, record) in records
.iter()
.filter(|(subject, _)| subject == TOPOLOGY_SUBJECT)
{
let this = &record["devices"][0];
assert!(this.get("started").is_none(), "{this}");
let fault = this["started_fault"]
.as_str()
.ok_or_else(|| format!("no started_fault: {this}"))?;
assert!(fault.starts_with("console clock: "), "{fault}");
}
assert_eq!(
hub.counts().topology_clock_failures,
2,
"each document sent with the fault is counted"
);
Ok(())
}
#[test]
fn a_worker_named_as_the_exchange_or_the_depot_is_kept_apart_and_counted() -> TestResult {
let started = Instant::now();
let model = TopologyModel::new(
ThisDevice {
id: "local".to_owned(),
liminal: "1.2.3".to_owned(),
started,
depot: true,
},
[],
[],
[],
[],
);
let hub = ConsoleHub::with_capacity("local", model, WallBase::fixed(1_000, started), 8)?;
let out = Scripted::default();
hub.attach(TOPOLOGY_SUBJECT)?;
hub.attach(ORDERS_TAP)?;
hub.emit(TopologyChange::Channel {
name: "orders".to_owned(),
life: LineLife::Kept,
state: LineState::Live,
})?;
for (connection, identity) in [(7, "local"), (8, "local.depot")] {
hub.emit(TopologyChange::Worker {
connection,
identity: identity.to_owned(),
})?;
hub.emit(TopologyChange::Flow {
connection,
line: "orders".to_owned(),
role: FlowRole::Subscribes,
present: true,
})?;
}
publish(&hub, 1)?;
assert_eq!(hub.counts().station_collisions, 2);
hub.pump(&out)?;
let sent = out.take();
let document = &sent
.iter()
.rev()
.find(|(subject, _)| subject == TOPOLOGY_SUBJECT)
.ok_or("no topology document")?
.1;
let kinds: Vec<(&str, &str)> = document["stations"]
.as_array()
.ok_or("no stations")?
.iter()
.filter_map(|station| Some((station["id"].as_str()?, station["kind"].as_str()?)))
.collect();
assert_eq!(kinds, [("local", "exchange"), ("local.depot", "depot")]);
assert_eq!(
document["lines"][0]["stops"],
serde_json::json!(["local.depot", "local"])
);
let from: Vec<Option<Value>> = sent
.iter()
.filter(|(subject, _)| subject == ORDERS_TAP)
.flat_map(|(_, record)| record["messages"].as_array().cloned().unwrap_or_default())
.map(|message| message.get("from_station").cloned())
.collect();
assert_eq!(from, [None], "the tap names no station for it");
Ok(())
}