use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant};
use crate::zmq_context::shared_context;
use crate::zmq_readout::{max_readout_float_limit, parse_readout_packet};
use crate::{BackendError, IpcBackend};
const DEFAULT_READOUT_IPC: &str = "ipc:///tmp/corpus_ipc_readout.ipc";
struct ExclusiveSocket {
socket: zmq::Socket,
}
const READOUT_RCVHWM: i32 = 16;
const RECEIVE_DRAIN_MAX_MESSAGES: u32 = 64; const RECEIVE_DRAIN_MAX_DURATION: Duration = Duration::from_millis(5);
pub const DEFAULT_RECONNECT_IVL_MS: i32 = 100;
pub const DEFAULT_RECONNECT_IVL_MAX_MS: i32 = 5_000;
pub const ENV_RECONNECT_IVL_MS: &str = "CORPUS_IPC_ZMQ_RECONNECT_IVL_MS";
pub const ENV_RECONNECT_IVL_MAX_MS: &str = "CORPUS_IPC_ZMQ_RECONNECT_IVL_MAX_MS";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ReconnectConfig {
ivl_ms: i32,
ivl_max_ms: i32,
}
fn parse_positive_ms(raw: Option<String>, default: i32) -> i32 {
raw.and_then(|raw| raw.parse::<i32>().ok())
.filter(|&v| v > 0)
.unwrap_or(default)
}
fn env_i32_or(name: &str, default: i32) -> i32 {
parse_positive_ms(std::env::var(name).ok(), default)
}
fn reconnect_config_from_env() -> ReconnectConfig {
ReconnectConfig {
ivl_ms: env_i32_or(ENV_RECONNECT_IVL_MS, DEFAULT_RECONNECT_IVL_MS),
ivl_max_ms: env_i32_or(ENV_RECONNECT_IVL_MAX_MS, DEFAULT_RECONNECT_IVL_MAX_MS),
}
}
impl ExclusiveSocket {
fn recv_bytes_dontwait(&mut self) -> zmq::Result<Vec<u8>> {
self.socket.recv_bytes(zmq::DONTWAIT)
}
}
#[deprecated(since = "0.1.0", note = "renamed to ZmqIpcBackend")]
pub type ZmqRuntimeBackend = ZmqIpcBackend;
pub struct ZmqIpcBackend {
context: zmq::Context,
sub_socket: Mutex<Option<ExclusiveSocket>>,
initialized: bool,
pub(crate) last_readout: Vec<f32>,
pub tick: i64,
skipped_readouts: u64,
malformed_readouts: u64,
}
impl ZmqIpcBackend {
pub fn new() -> Self {
Self {
context: shared_context(),
sub_socket: Mutex::new(None),
initialized: false,
last_readout: Vec::new(),
tick: 0,
skipped_readouts: 0,
malformed_readouts: 0,
}
}
pub fn tick(&self) -> i64 {
self.tick
}
pub fn skipped_readouts(&self) -> u64 {
self.skipped_readouts
}
pub fn malformed_readouts(&self) -> u64 {
self.malformed_readouts
}
fn lock_sub_socket(&self) -> MutexGuard<'_, Option<ExclusiveSocket>> {
self.sub_socket
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn connect_sub_with(
&mut self,
endpoint: &str,
cfg: ReconnectConfig,
) -> Result<(), BackendError> {
let socket = self
.context
.socket(zmq::SUB)
.map_err(|e| BackendError::InitializationError(format!("ZMQ SUB socket: {e}")))?;
socket
.set_subscribe(b"")
.map_err(|e| BackendError::InitializationError(format!("ZMQ subscribe: {e}")))?;
socket
.set_rcvhwm(READOUT_RCVHWM)
.map_err(|e| BackendError::InitializationError(format!("ZMQ rcvhwm: {e}")))?;
socket
.set_reconnect_ivl(cfg.ivl_ms)
.map_err(|e| BackendError::InitializationError(format!("ZMQ reconnect_ivl: {e}")))?;
socket.set_reconnect_ivl_max(cfg.ivl_max_ms).map_err(|e| {
BackendError::InitializationError(format!("ZMQ reconnect_ivl_max: {e}"))
})?;
socket.connect(endpoint).map_err(|e| {
BackendError::InitializationError(format!(
"ZMQ connect to {endpoint}: {e} (is the IPC producer running?)"
))
})?;
*self.lock_sub_socket() = Some(ExclusiveSocket { socket });
Ok(())
}
fn connect_sub(&mut self, endpoint: &str) -> Result<(), BackendError> {
self.connect_sub_with(endpoint, reconnect_config_from_env())
}
#[cfg(test)]
fn initialize_at(&mut self, endpoint: &str) -> Result<(), BackendError> {
if self.initialized {
return Ok(());
}
self.connect_sub_with(
endpoint,
ReconnectConfig {
ivl_ms: 10,
ivl_max_ms: 100,
},
)?;
self.initialized = true;
Ok(())
}
fn ingest_readout_packet_with_limit(
&mut self,
buf: &[u8],
max_floats: usize,
) -> Result<(), BackendError> {
let (tick, readout) = parse_readout_packet(buf, max_floats)?;
self.tick = tick;
self.last_readout.clear();
self.last_readout.extend_from_slice(&readout);
Ok(())
}
fn ingest_readout_packet(&mut self, buf: &[u8]) -> Result<(), BackendError> {
self.ingest_readout_packet_with_limit(buf, max_readout_float_limit())
}
fn drain_budget_exhausted(received: u32, started: Instant) -> bool {
if received >= RECEIVE_DRAIN_MAX_MESSAGES {
return true;
}
if received == 0 {
return false;
}
started.elapsed() >= RECEIVE_DRAIN_MAX_DURATION
}
fn try_recv_readout_packet(&self) -> Result<Option<Vec<u8>>, BackendError> {
let mut guard = self.lock_sub_socket();
let socket = guard.as_mut().ok_or_else(|| {
BackendError::CommunicationError("SUB socket not connected".to_string())
})?;
match socket.recv_bytes_dontwait() {
Ok(buf) => Ok(Some(buf)),
Err(zmq::Error::EAGAIN) => Ok(None),
Err(e) => Err(BackendError::CommunicationError(format!(
"ZMQ recv failed: {e}"
))),
}
}
fn parse_received_readout(&mut self, buf: &[u8]) -> Result<(i64, Vec<f32>), BackendError> {
parse_readout_packet(buf, max_readout_float_limit()).inspect_err(|_| {
self.malformed_readouts = self.malformed_readouts.saturating_add(1);
})
}
fn receive_readout(&mut self) -> Result<Vec<f32>, BackendError> {
let mut newest = None;
let started = Instant::now();
let mut received = 0u32;
loop {
if Self::drain_budget_exhausted(received, started) {
break;
}
let Some(buf) = self.try_recv_readout_packet()? else {
break;
};
received = received.saturating_add(1);
let parsed = self.parse_received_readout(&buf)?;
if newest.replace(parsed).is_some() {
self.skipped_readouts = self.skipped_readouts.saturating_add(1);
}
}
if let Some((tick, readout)) = newest {
self.tick = tick;
self.last_readout = readout;
}
Ok(self.last_readout.clone())
}
#[doc(hidden)]
pub fn apply_readout_packet_for_tests(&mut self, buf: &[u8]) -> Result<(), BackendError> {
self.ingest_readout_packet(buf)
}
#[doc(hidden)]
pub fn apply_readout_packet_with_limit_for_tests(
&mut self,
buf: &[u8],
max_floats: usize,
) -> Result<(), BackendError> {
self.ingest_readout_packet_with_limit(buf, max_floats)
}
#[doc(hidden)]
pub fn readout_cache_snapshot_for_tests(&self) -> (i64, Vec<f32>) {
(self.tick, self.last_readout.clone())
}
}
impl Default for ZmqIpcBackend {
fn default() -> Self {
Self::new()
}
}
impl IpcBackend for ZmqIpcBackend {
fn process_batch(&mut self, _inputs: &[f32]) -> Result<Vec<f32>, BackendError> {
if !self.initialized {
return Err(BackendError::InitializationError(
"ZmqIpcBackend not initialized — call initialize() first".to_string(),
));
}
self.receive_readout()
}
fn initialize(&mut self, _model_path: Option<&str>) -> Result<(), BackendError> {
if self.initialized {
return Ok(());
}
let endpoint = std::env::var("CORPUS_IPC_ZMQ_READOUT_IPC")
.unwrap_or_else(|_| DEFAULT_READOUT_IPC.to_string());
self.connect_sub(&endpoint)?;
self.initialized = true;
println!("[zmq-ipc] Connected to IPC producer at {}", endpoint);
Ok(())
}
fn save_state(&self, _model_path: &str) -> Result<(), BackendError> {
Ok(())
}
fn get_spike_states(&self) -> Vec<bool> {
self.last_readout.iter().map(|&v| v > 0.5).collect()
}
fn reset(&mut self) -> Result<(), BackendError> {
self.last_readout.clear();
self.tick = 0;
self.skipped_readouts = 0;
self.malformed_readouts = 0;
self.initialized = false;
*self.lock_sub_socket() = None;
println!("[zmq-ipc] Readout cache reset; will re-initialize on next call");
Ok(())
}
}
const _: fn() = _assert_intended_send_sync_surface;
fn _assert_intended_send_sync_surface() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_ipc_backend<T: IpcBackend>() {}
assert_send::<zmq::Socket>();
assert_send::<ExclusiveSocket>();
assert_send::<ZmqIpcBackend>();
assert_sync::<ZmqIpcBackend>();
assert_ipc_backend::<ZmqIpcBackend>();
let _ = <zmq::Socket as _assert_not_sync::IfSyncThenAmbiguous<_>>::witness;
let _ = <ExclusiveSocket as _assert_not_sync::IfSyncThenAmbiguous<_>>::witness;
}
mod _assert_not_sync {
pub trait IfSyncThenAmbiguous<Marker> {
fn witness() {}
}
pub struct NotSyncToken;
pub struct SyncToken;
impl<T: ?Sized> IfSyncThenAmbiguous<NotSyncToken> for T {}
impl<T: ?Sized + Sync> IfSyncThenAmbiguous<SyncToken> for T {}
}
#[cfg(test)]
mod tests {
fn wait_until(timeout: Duration, mut pred: impl FnMut() -> bool) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if pred() {
return true;
}
std::thread::sleep(Duration::from_millis(1));
}
pred()
}
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use super::*;
use crate::zmq_context::shared_context;
fn unique_test_endpoint() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
format!(
"ipc:///tmp/corpus_ipc_zmq_test_{}_{}.ipc",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
)
}
fn make_packet(tick: i64, readout: &[f32]) -> Vec<u8> {
let mut buf = Vec::with_capacity(8 + readout.len() * 4);
buf.extend_from_slice(&tick.to_le_bytes());
for v in readout {
buf.extend_from_slice(&v.to_le_bytes());
}
buf
}
fn connected_pub_sub() -> (zmq::Socket, ZmqIpcBackend) {
let endpoint = unique_test_endpoint();
let context = shared_context();
let publisher = context.socket(zmq::PUB).unwrap();
publisher.bind(&endpoint).unwrap();
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&endpoint).unwrap();
for _ in 0..100 {
publisher.send(make_packet(1, &[1.0]), 0).unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
backend.process_batch(&[]).unwrap();
if backend.tick() == 1 {
return (publisher, backend);
}
}
panic!("ZMQ subscription did not become ready");
}
fn publish_until_seen(
publisher: &zmq::Socket,
backend: &mut ZmqIpcBackend,
tick: i64,
timeout: Duration,
) -> bool {
let packet = make_packet(tick, &[tick as f32]);
wait_until(timeout, || {
publisher.send(packet.clone(), 0).unwrap();
backend.process_batch(&[]).unwrap();
backend.tick() == tick
})
}
#[test]
fn late_publisher_startup_recovers_via_libzmq_reconnect() {
let endpoint = unique_test_endpoint();
let mut backend = ZmqIpcBackend::new();
backend
.initialize_at(&endpoint)
.expect("connect is asynchronous before the publisher binds");
assert!(
backend.process_batch(&[]).unwrap().is_empty(),
"no publisher yet: empty cache on EAGAIN"
);
let context = shared_context();
let publisher = context.socket(zmq::PUB).unwrap();
publisher.bind(&endpoint).unwrap();
assert!(
publish_until_seen(&publisher, &mut backend, 7, Duration::from_secs(5)),
"timed out waiting for libzmq reconnect after late publisher bind"
);
assert_eq!(backend.last_readout, vec![7.0]);
}
#[test]
fn publisher_restart_on_same_ipc_endpoint_recovers() {
let context = shared_context();
let first = context.socket(zmq::PUB).unwrap();
first.bind(&unique_test_endpoint()).unwrap();
publisher_restart_on_same_endpoint_recovers(&context, first);
}
#[test]
fn publisher_restart_on_same_tcp_endpoint_recovers() {
let context = shared_context();
let first = context.socket(zmq::PUB).unwrap();
first.bind("tcp://127.0.0.1:0").unwrap();
publisher_restart_on_same_endpoint_recovers(&context, first);
}
fn publisher_restart_on_same_endpoint_recovers(context: &zmq::Context, first: zmq::Socket) {
let endpoint = first
.get_last_endpoint()
.unwrap()
.expect("publisher is bound");
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&endpoint).unwrap();
{
assert!(
publish_until_seen(&first, &mut backend, 1, Duration::from_secs(5)),
"timed out waiting for the first publisher"
);
drop(first);
}
let second = context.socket(zmq::PUB).unwrap();
let bound = wait_until(Duration::from_secs(5), || second.bind(&endpoint).is_ok());
assert!(bound, "timed out rebinding {endpoint} after publisher drop");
assert!(
publish_until_seen(&second, &mut backend, 2, Duration::from_secs(10)),
"timed out waiting for libzmq reconnect after publisher restart"
);
assert_eq!(backend.last_readout, vec![2.0]);
}
#[test]
fn reset_then_initialize_at_replacement_endpoint() {
let stale_endpoint = unique_test_endpoint();
let live_endpoint = unique_test_endpoint();
let context = shared_context();
let publisher = context.socket(zmq::PUB).unwrap();
publisher.bind(&live_endpoint).unwrap();
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&stale_endpoint).unwrap();
backend.reset().unwrap();
assert_eq!(backend.readout_cache_snapshot_for_tests(), (0, vec![]));
backend.initialize_at(&live_endpoint).unwrap();
assert!(
publish_until_seen(&publisher, &mut backend, 5, Duration::from_secs(5)),
"timed out waiting for delivery on the replacement endpoint"
);
assert_eq!(backend.last_readout, vec![5.0]);
}
#[test]
fn parse_dynamic_packet_via_production_ingest() {
let max_cap = crate::zmq_readout::max_readout_float_limit();
let count = max_cap.clamp(1, 20);
let readout: Vec<f32> = (0..count).map(|i| i as f32 * 0.1).collect();
let tick: i64 = 42_000;
let buf = make_packet(tick, &readout);
let mut b = ZmqIpcBackend::new();
b.apply_readout_packet_for_tests(&buf)
.expect("valid dynamic frame within configured limit");
assert_eq!(b.tick, tick);
assert_eq!(b.last_readout.len(), count);
for (i, val) in readout.iter().enumerate().take(count) {
assert!((b.last_readout[i] - val).abs() < 1e-5);
}
}
#[test]
fn reconnect_ms_parsing_accepts_positive_ints_only() {
assert_eq!(parse_positive_ms(Some("250".into()), 100), 250);
assert_eq!(parse_positive_ms(Some("1".into()), 100), 1);
assert_eq!(parse_positive_ms(None, 100), 100);
assert_eq!(parse_positive_ms(Some("nope".into()), 100), 100);
assert_eq!(parse_positive_ms(Some("0".into()), 100), 100);
assert_eq!(parse_positive_ms(Some("-5".into()), 100), 100);
assert_eq!(parse_positive_ms(Some("1.5".into()), 100), 100);
}
#[test]
#[allow(deprecated)]
fn deprecated_alias_is_same_type() {
fn same(backend: crate::ZmqRuntimeBackend) -> ZmqIpcBackend {
backend
}
let _ = same(ZmqIpcBackend::new());
}
#[test]
fn malformed_packet_does_not_mutate_state() {
let mut b = ZmqIpcBackend::new();
b.last_readout = vec![1.0, 2.0];
b.tick = 100;
let initial_readout = b.last_readout.clone();
let initial_tick = b.tick;
let bad_buf: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x00];
let err = b
.apply_readout_packet_for_tests(bad_buf)
.expect_err("truncated packet must fail");
assert!(
matches!(err, BackendError::CommunicationError(_)),
"malformed framing is not EAGAIN: {err:?}"
);
assert_eq!(b.last_readout, initial_readout);
assert_eq!(b.tick, initial_tick);
}
#[test]
fn process_batch_drains_beyond_hwm_to_newest_accepted_frame() {
let (publisher, mut backend) = connected_pub_sub();
let packet_count = READOUT_RCVHWM as i64 * 4;
let skipped_before = backend.skipped_readouts();
for tick in 2..=packet_count + 1 {
publisher
.send(make_packet(tick, &[tick as f32]), 0)
.unwrap();
}
assert!(
wait_until(Duration::from_millis(500), || {
backend.process_batch(&[]).unwrap();
backend.tick() > 1 && backend.skipped_readouts() > skipped_before
}),
"timed out waiting for a burst with superseded readouts"
);
let output = backend.last_readout.clone();
let newest_accepted_tick = backend.tick();
assert!(newest_accepted_tick > 1);
assert_eq!(output, vec![newest_accepted_tick as f32]);
assert!(backend.skipped_readouts() > 0);
}
#[test]
fn malformed_frame_in_drained_burst_fails_closed_without_applying_cache() {
let (publisher, mut backend) = connected_pub_sub();
let malformed_before = backend.malformed_readouts();
publisher.send(make_packet(2, &[2.0]), 0).unwrap();
publisher
.send([0xde, 0xad, 0xbe, 0xef].as_slice(), 0)
.unwrap();
publisher.send(make_packet(3, &[3.0]), 0).unwrap();
assert!(
wait_until(Duration::from_millis(500), || {
let before = backend.readout_cache_snapshot_for_tests();
match backend.process_batch(&[]) {
Err(BackendError::CommunicationError(_)) => {
assert_eq!(backend.readout_cache_snapshot_for_tests(), before);
true
}
Ok(_) => false,
Err(e) => panic!("unexpected error: {e:?}"),
}
}),
"timed out waiting for malformed frame"
);
assert!(
wait_until(Duration::from_millis(500), || {
backend.process_batch(&[]).unwrap() == vec![3.0]
}),
"timed out waiting for tick-3 frame"
);
assert_eq!(backend.tick(), 3);
assert_eq!(backend.malformed_readouts() - malformed_before, 1);
backend.reset().unwrap();
assert_eq!(backend.malformed_readouts(), 0);
assert_eq!(backend.skipped_readouts(), 0);
assert_eq!(backend.tick(), 0);
}
#[test]
fn over_limit_frame_in_drained_burst_fails_closed() {
let (publisher, mut backend) = connected_pub_sub();
let before = backend.readout_cache_snapshot_for_tests();
let max_floats = max_readout_float_limit();
publisher
.send(make_packet(2, &vec![0.0; max_floats + 1]), 0)
.unwrap();
assert!(
wait_until(Duration::from_millis(500), || {
match backend.process_batch(&[]) {
Err(BackendError::InvalidInput(_)) => true,
Ok(_) => false,
Err(e) => panic!("unexpected error: {e:?}"),
}
}),
"timed out waiting for over-limit frame"
);
assert_eq!(backend.readout_cache_snapshot_for_tests(), before);
assert_eq!(backend.malformed_readouts(), 1);
}
#[test]
fn process_batch_before_initialize_returns_initialization_error() {
let mut backend = ZmqIpcBackend::new();
let err = backend.process_batch(&[]).unwrap_err();
assert!(matches!(err, BackendError::InitializationError(_)));
}
#[test]
fn initialize_without_publisher_is_nonblocking() {
let mut backend = ZmqIpcBackend::new();
backend.initialize(None).unwrap();
assert!(backend.process_batch(&[]).unwrap().is_empty());
backend.reset().unwrap();
assert!(matches!(
backend.process_batch(&[]),
Err(BackendError::InitializationError(_))
));
}
#[test]
fn missing_sub_socket_reports_communication_error() {
let backend = ZmqIpcBackend::new();
assert!(matches!(
backend.try_recv_readout_packet(),
Err(BackendError::CommunicationError(_))
));
}
#[test]
fn get_spike_states_thresholds_last_readout() {
let (publisher, mut backend) = connected_pub_sub();
let max_cap = crate::zmq_readout::max_readout_float_limit();
let values: Vec<f32> = if max_cap >= 3 {
vec![0.25, 0.75, 0.51]
} else {
vec![0.75]
};
let expected: Vec<bool> = values.iter().map(|&v| v > 0.5).collect();
let expected_len = values.len();
publisher.send(make_packet(9, &values), 0).unwrap();
assert!(
wait_until(Duration::from_millis(200), || {
backend
.process_batch(&[])
.is_ok_and(|out| out.len() == expected_len)
}),
"timed out waiting for multi-channel readout"
);
assert_eq!(backend.get_spike_states(), expected);
}
#[test]
fn receive_drain_bound_returns_under_sustained_publish_load() {
let (publisher, mut backend) = connected_pub_sub();
let running = Arc::new(AtomicBool::new(true));
let flooder = {
let running = Arc::clone(&running);
std::thread::spawn(move || {
let mut tick = 2i64;
while running.load(Ordering::Relaxed) {
let _ = publisher.send(make_packet(tick, &[tick as f32]), zmq::DONTWAIT);
tick += 1;
std::thread::yield_now();
}
})
};
let deadline = Instant::now() + Duration::from_secs(5);
let mut worst = Duration::ZERO;
let mut output = Vec::new();
let mut skipped_at_flood: Option<u64> = None;
let mut backlog_drained = false;
while !backlog_drained && Instant::now() < deadline {
let started = Instant::now();
output = backend.process_batch(&[]).unwrap();
worst = worst.max(started.elapsed());
if backend.tick() > 1 {
let baseline = *skipped_at_flood.get_or_insert(backend.skipped_readouts());
backlog_drained = backend.skipped_readouts() > baseline;
}
std::thread::sleep(Duration::from_micros(200));
}
running.store(false, Ordering::Relaxed);
flooder.join().expect("flooder panicked");
assert!(
worst < Duration::from_millis(100),
"bounded drain must return promptly under sustained publish load"
);
assert!(
backlog_drained,
"no drain faced a backlog of flooded frames"
);
assert!(backend.tick() > 1, "no flooded frame was observed");
assert_eq!(output[0], backend.tick() as f32);
}
#[test]
fn drain_budget_always_allows_first_receive_attempt() {
let expired = Instant::now() - RECEIVE_DRAIN_MAX_DURATION;
assert!(!ZmqIpcBackend::drain_budget_exhausted(0, expired));
assert!(ZmqIpcBackend::drain_budget_exhausted(1, expired));
assert!(ZmqIpcBackend::drain_budget_exhausted(
RECEIVE_DRAIN_MAX_MESSAGES,
Instant::now()
));
}
#[test]
fn over_limit_packet_is_invalid_input_without_mutating_state() {
let max = 4;
let buf = make_packet(1, &[0.0; 5]);
let mut b = ZmqIpcBackend::new();
b.last_readout = vec![1.0, 2.0];
b.tick = 100;
let before = b.readout_cache_snapshot_for_tests();
let err = b
.apply_readout_packet_with_limit_for_tests(&buf, max)
.expect_err("five floats exceeds cap of four");
assert!(matches!(err, BackendError::InvalidInput(_)));
assert_eq!(b.readout_cache_snapshot_for_tests(), before);
}
#[test]
fn initialize_nonblocking_recv_and_drop() {
let mut backend = ZmqIpcBackend::new();
backend
.initialize_at(&unique_test_endpoint())
.expect("ZMQ connect is asynchronous; a publisher need not be bound");
let out = backend.process_batch(&[1.0]).unwrap();
assert!(out.is_empty());
assert_eq!(backend.tick(), 0);
drop(backend);
}
#[test]
fn connected_backend_can_move_to_owner_thread() {
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&unique_test_endpoint()).unwrap();
let readout = std::thread::spawn(move || {
let out = backend.process_batch(&[]).unwrap();
drop(backend);
out
})
.join()
.expect("owner thread panicked");
assert!(readout.is_empty());
}
#[test]
fn uninitialized_backend_can_move_then_initialize() {
let backend = ZmqIpcBackend::new();
let endpoint = unique_test_endpoint();
std::thread::spawn(move || {
let mut backend = backend;
backend.initialize_at(&endpoint).unwrap();
let out = backend.process_batch(&[]).unwrap();
assert!(out.is_empty());
})
.join()
.expect("owner thread panicked");
}
#[test]
fn repeated_construct_initialize_process_drop() {
for _ in 0..8 {
let mut backend = ZmqIpcBackend::new();
backend.initialize_at(&unique_test_endpoint()).unwrap();
let out = backend.process_batch(&[]).unwrap();
assert!(out.is_empty());
backend.reset().unwrap();
drop(backend);
}
}
#[test]
fn backend_is_usable_behind_arc_mutex() {
let backend = Arc::new(Mutex::new(ZmqIpcBackend::new()));
{
let mut guard = backend.lock().unwrap();
guard.initialize_at(&unique_test_endpoint()).unwrap();
assert!(guard.process_batch(&[]).unwrap().is_empty());
}
let moved = Arc::clone(&backend);
std::thread::spawn(move || {
let mut guard = moved.lock().unwrap();
assert!(guard.process_batch(&[]).unwrap().is_empty());
guard.reset().unwrap();
})
.join()
.expect("mutex owner thread panicked");
}
#[test]
fn thread_safety_surface_is_the_documented_one() {
_assert_intended_send_sync_surface();
}
}