use std::error::Error;
use std::path::Path;
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use liminal::Envelope;
use liminal::channel::ClusterObserver;
use liminal::conversation::EchoBehaviour;
use liminal::protocol::{CausalContext, MessageEnvelope, SchemaId};
use super::hub::HubLock;
use super::{ConsoleCountsSnapshot, TOPOLOGY_SUBJECT};
use crate::config::types::{ChannelDef, LimitsConfig, ServerConfig, ServicesConfig};
use crate::server::connection::services::{
ConnectionServices, LiminalConnectionServices, roster_reads,
};
type TestResult = Result<(), Box<dyn Error>>;
const ORDERS_TAP: &str = "console.tap.orders";
fn channel(name: &str) -> ChannelDef {
ChannelDef {
name: name.to_owned(),
schema_ref: None,
durable: false,
loaded_schema: None,
}
}
fn services(persistence_path: &Path) -> Result<LiminalConnectionServices, Box<dyn Error>> {
services_with(
vec![
channel("orders"),
channel(TOPOLOGY_SUBJECT),
channel(ORDERS_TAP),
],
persistence_path,
)
}
fn services_with(
channels: Vec<ChannelDef>,
persistence_path: &Path,
) -> Result<LiminalConnectionServices, Box<dyn Error>> {
Ok(LiminalConnectionServices::from_config(&ServerConfig {
listen_address: "127.0.0.1:0".parse()?,
health_listen_address: "127.0.0.1:0".parse()?,
drain_timeout_ms: 30_000,
channels,
routing_rules: Vec::new(),
persistence_path: Some(persistence_path.to_path_buf()),
cluster: None,
auth: None,
services: ServicesConfig::default(),
limits: LimitsConfig::default(),
participant: None,
websocket: None,
})?)
}
fn envelope() -> MessageEnvelope {
MessageEnvelope::new(
SchemaId::new([0_u8; SchemaId::WIRE_LEN]),
CausalContext::independent(),
br#"{"order":1}"#.to_vec(),
)
}
fn counts(services: &LiminalConnectionServices) -> ConsoleCountsSnapshot {
services.console().counts()
}
#[derive(Debug, Default)]
struct Departures {
seen: Mutex<u64>,
changed: Condvar,
}
impl Departures {
fn install(services: &LiminalConnectionServices) -> Result<Arc<Self>, Box<dyn Error>> {
let departures = Arc::new(Self::default());
services
.channel_cluster()
.observer()
.set_witness(Arc::clone(&departures) as Arc<dyn ClusterObserver>)?;
Ok(departures)
}
fn wait_for(&self, target: u64) -> TestResult {
let (seen, guard) = self
.changed
.wait_timeout_while(
self.seen.lock().map_err(|_| "departures poisoned")?,
Duration::from_secs(60),
|seen| *seen < target,
)
.map_err(|_| "departures poisoned")?;
let count = *seen;
drop(seen);
if guard.timed_out() {
return Err(format!("{count} departures, expected {target}").into());
}
Ok(())
}
}
impl ClusterObserver for Departures {
fn on_subscribe(&self, _channel: &str, _subscriber_pid: u64) {}
fn on_unsubscribe(&self, _channel: &str, _subscriber_pid: u64) {
if let Ok(mut seen) = self.seen.lock() {
*seen = seen.saturating_add(1);
}
self.changed.notify_all();
}
fn on_publish(&self, _channel: &str, _envelope: &Envelope) {}
}
#[test]
fn a_poisoned_topology_counts_one_emit_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
assert_eq!(counts(&services).topology_emit_failures, 0);
assert!(services.console().poison(HubLock::Topology));
services.register_responder("orders.reply", Arc::new(EchoBehaviour))?;
assert!(
services
.responder_subjects()?
.contains(&"orders.reply".to_owned())
);
assert_eq!(counts(&services).topology_emit_failures, 1);
Ok(())
}
#[test]
fn a_poisoned_tap_counts_one_observe_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
let _taker = services.subscribe("orders", &[], None)?;
let _tap = services.subscribe(ORDERS_TAP, &[], None)?;
assert_eq!(counts(&services).tap_observe_failures, 0);
assert!(services.console().tap().poison_subscribers());
let outcome = services.publish(7, "orders", &envelope(), None)?;
assert!(
outcome.delivered,
"the publish answers as it would without the console"
);
assert_eq!(counts(&services).tap_observe_failures, 1);
Ok(())
}
#[test]
fn a_poisoned_tap_table_counts_one_attach_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
assert!(services.console().poison(HubLock::Taps));
let subscription = services.subscribe(ORDERS_TAP, &[], None);
assert!(
subscription.is_ok(),
"the subscribe answers as it would without the console"
);
assert_eq!(counts(&services).tap_attach_failures, 1);
Ok(())
}
#[test]
fn a_poisoned_tap_table_counts_one_detach_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
let departures = Departures::install(&services)?;
let subscription = services.subscribe(ORDERS_TAP, &[], None)?;
assert_eq!(counts(&services).tap_detach_failures, 0);
assert!(services.console().poison(HubLock::Taps));
services.unsubscribe(subscription)?;
departures.wait_for(1)?;
assert_eq!(counts(&services).tap_detach_failures, 1);
Ok(())
}
#[test]
fn a_poisoned_notifier_counts_one_wake_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
let _topology = services.subscribe(TOPOLOGY_SUBJECT, &[], None)?;
assert_eq!(counts(&services).console_wake_failures, 0);
assert!(services.console().poison(HubLock::Notifier));
services.register_responder("orders.reply", Arc::new(EchoBehaviour))?;
assert_eq!(counts(&services).console_wake_failures, 1);
Ok(())
}
#[test]
fn a_poisoned_subscriber_list_counts_one_unsubscribe_failure() -> TestResult {
let home = tempfile::tempdir()?;
let services = services(home.path())?;
let departures = Departures::install(&services)?;
let subscription = services.subscribe(ORDERS_TAP, &[], None)?;
assert_eq!(counts(&services).tap_unsubscribe_failures, 0);
assert!(services.console().tap().poison_subscribers());
services.unsubscribe(subscription)?;
departures.wait_for(1)?;
assert_eq!(counts(&services).tap_unsubscribe_failures, 1);
Ok(())
}
#[test]
fn the_snapshot_reads_the_roster_once() -> TestResult {
let home = tempfile::tempdir()?;
let services = services_with(
(0..20)
.map(|index| channel(&format!("fixture.{index:02}")))
.collect(),
home.path(),
)?;
let before = roster_reads();
let snapshot = super::snapshot(&services)?;
assert_eq!(roster_reads() - before, 1);
assert_eq!(snapshot.channels.len(), 20);
Ok(())
}