use crate::tests::packed_frame_type;
use crate::{
TelemetryResult,
config::{DataEndpoint, DataType},
packet::Packet,
router::{Clock, EndpointHandler, Router, RouterConfig},
wire_format,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Duration;
fn zero_clock() -> Box<dyn Clock + Send + Sync> {
Box::new(|| 0u64)
}
#[test]
fn router_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Router>();
}
#[test]
fn concurrent_rx_queue_is_thread_safe() {
const THREADS: usize = 4;
const ITERS_PER_THREAD: usize = 50;
let total = THREADS * ITERS_PER_THREAD;
let hits = Arc::new(AtomicUsize::new(0));
let hits_c = hits.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
hits_c.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let router = Router::new_with_clock(RouterConfig::new(vec![handler]), zero_clock());
let r = Arc::new(router);
let mut threads_vec = Vec::new();
for tid in 0..THREADS {
let r_cloned = r.clone();
threads_vec.push(thread::spawn(move || {
for i in 0..ITERS_PER_THREAD {
let idx = (tid * ITERS_PER_THREAD + i) as u64;
let base = 1.0_f32 + idx as f32 * 0.001;
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[base, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
idx,
)
.unwrap();
r_cloned.rx_queue(pkt).unwrap();
}
}));
}
for t in threads_vec {
t.join().expect("producer thread panicked");
}
r.process_rx_queue().unwrap();
assert_eq!(
hits.load(Ordering::SeqCst),
total,
"expected {total} handler invocations from RX queue"
);
}
#[test]
fn rtos_like_ingress_and_processing_no_deadlock() {
const THREADS: usize = 4;
const ITERS_PER_THREAD: usize = 80;
let total = THREADS * ITERS_PER_THREAD;
let hits = Arc::new(AtomicUsize::new(0));
let hits_c = hits.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
hits_c.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let router = Arc::new(Router::new_with_clock(
RouterConfig::new(vec![handler]),
zero_clock(),
));
let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let proc_router = router.clone();
let done_c = done.clone();
let processor = thread::spawn(move || {
while !done_c.load(Ordering::SeqCst) {
proc_router.process_all_queues_with_timeout(1).unwrap();
}
proc_router.process_all_queues().unwrap();
});
let mut producers = Vec::new();
for tid in 0..THREADS {
let r = router.clone();
producers.push(thread::spawn(move || {
for i in 0..ITERS_PER_THREAD {
let idx = (tid * ITERS_PER_THREAD + i) as u64;
let base = 1.0_f32 + idx as f32 * 0.001;
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[base, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
idx,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
r.rx_packed_queue(&wire).unwrap();
}
}));
}
for t in producers {
t.join().expect("producer thread panicked");
}
done.store(true, Ordering::SeqCst);
processor.join().expect("processor thread panicked");
assert_eq!(
hits.load(Ordering::SeqCst),
total,
"expected {total} handler invocations from RTOS-like queued ingress"
);
}
#[test]
fn rtos_like_side_ingress_and_processing_no_deadlock() {
const THREADS: usize = 4;
const ITERS_PER_THREAD: usize = 60;
let total = THREADS * ITERS_PER_THREAD;
let hits = Arc::new(AtomicUsize::new(0));
let hits_c = hits.clone();
let local = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
hits_c.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let tx_count = Arc::new(AtomicUsize::new(0));
let tx_c0 = tx_count.clone();
let tx_c1 = tx_count.clone();
let router = Arc::new(Router::new_with_clock(
RouterConfig::new(vec![local]),
zero_clock(),
));
let side0 = router.add_side_packed("S0", move |_b| {
tx_c0.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let side1 = router.add_side_packed("S1", move |_b| {
tx_c1.fetch_add(1, Ordering::SeqCst);
Ok(())
});
assert_eq!(side0, 0);
assert_eq!(side1, 1);
let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let proc_router = router.clone();
let done_c = done.clone();
let processor = thread::spawn(move || {
while !done_c.load(Ordering::SeqCst) {
proc_router.process_all_queues_with_timeout(1).unwrap();
}
proc_router.process_all_queues().unwrap();
});
let mut producers = Vec::new();
for tid in 0..THREADS {
let r = router.clone();
producers.push(thread::spawn(move || {
for i in 0..ITERS_PER_THREAD {
let idx = (tid * ITERS_PER_THREAD + i) as u64;
let side = if (idx & 1) == 0 { 0 } else { 1 };
let base = 10.0_f32 + idx as f32 * 0.01;
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[base, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
idx,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
r.rx_packed_queue_from_side(&wire, side).unwrap();
}
}));
}
for t in producers {
t.join().expect("producer thread panicked");
}
done.store(true, Ordering::SeqCst);
processor.join().expect("processor thread panicked");
assert_eq!(
hits.load(Ordering::SeqCst),
total,
"expected local handler to see all side-tagged packets"
);
assert!(
tx_count.load(Ordering::SeqCst) > 0,
"relay mode should have forwarded packets to remote sides"
);
}
#[test]
fn handler_can_reenter_router_without_deadlock() {
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc;
let router_ref: Arc<OnceLock<Arc<Router>>> = Arc::new(OnceLock::new());
let triggered = Arc::new(AtomicBool::new(false));
let h1_hits = Arc::new(AtomicUsize::new(0));
let h2_hits = Arc::new(AtomicUsize::new(0));
let h1_hits_c = h1_hits.clone();
let h2_hits_c = h2_hits.clone();
let triggered_c = triggered.clone();
let router_ref_c = router_ref.clone();
let h1 = EndpointHandler::new_packet_handler(DataEndpoint::named("SD_CARD"), move |_pkt| {
h1_hits_c.fetch_add(1, Ordering::SeqCst);
if !triggered_c.swap(true, Ordering::SeqCst) {
let chained = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[9.0_f32, 8.0, 7.0],
&[DataEndpoint::named("RADIO")],
999,
)?;
let r = router_ref_c
.get()
.expect("router OnceLock should be initialized");
r.rx_queue(chained)?;
}
Ok(())
});
let h2 = EndpointHandler::new_packet_handler(DataEndpoint::named("RADIO"), move |_pkt| {
h2_hits_c.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let router = Arc::new(Router::new_with_clock(
RouterConfig::new(vec![h1, h2]),
zero_clock(),
));
router_ref
.set(router.clone())
.expect("router OnceLock should only be set once");
let first = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0_f32, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
100,
)
.unwrap();
router.rx_queue(first).unwrap();
let (tx_done, rx_done) = mpsc::channel();
let r = router.clone();
thread::spawn(move || {
let out = (|| -> TelemetryResult<()> {
r.process_rx_queue()?;
r.process_rx_queue()?;
Ok(())
})();
let _ = tx_done.send(out);
});
let done = rx_done
.recv_timeout(Duration::from_secs(2))
.expect("processing timed out (possible deadlock)");
done.expect("processing returned error");
assert_eq!(h1_hits.load(Ordering::SeqCst), 1);
assert_eq!(
h2_hits.load(Ordering::SeqCst),
1,
"chained callback enqueue should be processed exactly once"
);
}
#[test]
fn concurrent_receive_packed_is_thread_safe() {
const THREADS: usize = 4;
const ITERS_PER_THREAD: usize = 50;
let total = THREADS * ITERS_PER_THREAD;
let hits = Arc::new(AtomicUsize::new(0));
let hits_c = hits.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
hits_c.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let router = Router::new_with_clock(RouterConfig::new(vec![handler]), zero_clock());
let r = Arc::new(router);
let mut threads_vec = Vec::new();
for tid in 0..THREADS {
let r_cloned = r.clone();
threads_vec.push(thread::spawn(move || {
for i in 0..ITERS_PER_THREAD {
let idx = (tid * ITERS_PER_THREAD + i) as u64;
let base = 1.0_f32 + idx as f32 * 0.001;
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[base, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
idx,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
r_cloned.rx_packed(&wire).expect("receive_packed failed");
}
}));
}
for t in threads_vec {
t.join().expect("receive thread panicked");
}
assert_eq!(
hits.load(Ordering::SeqCst),
total,
"expected {total} handler invocations from receive_packed"
);
}
#[test]
fn concurrent_logging_and_processing_is_thread_safe() {
use std::thread;
const ITERS: usize = 200;
let tx_count = Arc::new(AtomicUsize::new(0));
let txc = tx_count.clone();
let tx = move |bytes: &[u8]| -> TelemetryResult<()> {
assert!(!bytes.is_empty());
if packed_frame_type(bytes) == Some(DataType::named("GPS_DATA")) {
txc.fetch_add(1, Ordering::SeqCst);
}
Ok(())
};
let rx_count = Arc::new(AtomicUsize::new(0));
let rxc = rx_count.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
rxc.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let router = Router::new_with_clock(RouterConfig::new(vec![handler]), zero_clock());
router.add_side_packed("tx", tx);
let r = Arc::new(router);
let r_logger = r.clone();
let logger = thread::spawn(move || {
for i in 0..ITERS {
r_logger
.log_queue(DataType::named("GPS_DATA"), &[1.0_f32, 5.9 + i as f32, 3.0])
.expect("log_queue failed");
}
});
let r_drain = r.clone();
let rx_counter = rx_count.clone();
let drainer = thread::spawn(move || {
while rx_counter.load(Ordering::SeqCst) < ITERS {
r_drain
.process_all_queues()
.expect("process_all_queues failed");
thread::yield_now();
}
});
logger.join().expect("logger thread panicked");
drainer.join().expect("drainer thread panicked");
let rx = rx_count.load(Ordering::SeqCst);
let tx = tx_count.load(Ordering::SeqCst);
assert_eq!(rx, ITERS, "expected {ITERS} handler calls, got {rx}");
assert_eq!(tx, ITERS, "expected {ITERS} TX frames, got {tx}");
}
#[test]
fn concurrent_log_receive_and_process_mix_is_thread_safe() {
use std::thread;
const LOG_ITERS: usize = 100;
const RX_ITERS: usize = 100;
let tx_count = Arc::new(AtomicUsize::new(0));
let txc = tx_count.clone();
let tx = move |bytes: &[u8]| -> TelemetryResult<()> {
assert!(!bytes.is_empty());
if packed_frame_type(bytes) == Some(DataType::named("GPS_DATA")) {
txc.fetch_add(1, Ordering::SeqCst);
}
Ok(())
};
let rx_count = Arc::new(AtomicUsize::new(0));
let rxc = rx_count.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
rxc.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let router = Router::new_with_clock(RouterConfig::new(vec![handler]), zero_clock());
router.add_side_packed("tx", tx);
let r = Arc::new(router);
let r_logger = r.clone();
let t_logger = thread::spawn(move || {
for i in 0..LOG_ITERS {
let base = 1.0_f32 + i as f32 * 0.01;
r_logger
.log_queue(DataType::named("GPS_DATA"), &[base, 2.0, 3.0])
.expect("log_queue failed");
}
});
let r_rx = r.clone();
let t_rx = thread::spawn(move || {
for i in 0..RX_ITERS {
let base = 4.0_f32 + i as f32 * 0.01;
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[base, 5.0, 6.0],
&[DataEndpoint::named("SD_CARD")],
i as u64,
)
.unwrap();
r_rx.rx_queue(pkt).expect("rx_packet_to_queue failed");
}
});
let r_proc = r.clone();
let rx_counter = rx_count.clone();
let t_proc = thread::spawn(move || {
while rx_counter.load(Ordering::SeqCst) < LOG_ITERS + RX_ITERS {
r_proc
.process_all_queues()
.expect("process_all_queues failed");
thread::yield_now();
}
});
t_logger.join().expect("logger thread panicked");
t_rx.join().expect("rx thread panicked");
t_proc.join().expect("processor thread panicked");
let tx = tx_count.load(Ordering::SeqCst);
let rx = rx_count.load(Ordering::SeqCst);
assert_eq!(tx, LOG_ITERS, "expected {LOG_ITERS} TX frames");
assert_eq!(
rx,
LOG_ITERS + RX_ITERS,
"expected {LOG_ITERS}+{RX_ITERS} handler invocations"
);
}