use crate::{Result, SdkError};
use serde::Serialize;
use zisk_common::io::ZiskStreamWriter;
use zisk_coordinator_client::{InputSender, InputSenderPushAdapter};
#[derive(Clone)]
pub struct ZiskStream {
writer: ZiskStreamWriter,
}
impl ZiskStream {
#[cfg(unix)]
pub fn unix() -> Self {
let path = format!("/tmp/zisk-input-{}.sock", uuid::Uuid::new_v4());
Self {
writer: ZiskStreamWriter::unix_at(&path)
.expect("failed to create UnixSocketStreamWriter"),
}
}
#[cfg(unix)]
pub fn unix_external(path: &str) -> Self {
let uri = format!("unix://{}", path);
Self { writer: ZiskStreamWriter::unix_external(uri) }
}
#[cfg(unix)]
pub fn unix_at(path: &str) -> Result<Self> {
Ok(Self { writer: ZiskStreamWriter::unix_at(path).map_err(SdkError::backend)? })
}
pub fn quic(uri: &str) -> Result<Self> {
let addr_str = uri.strip_prefix("quic://").ok_or_else(|| {
SdkError::InvalidConfig("QUIC URI must start with quic://".to_string())
})?;
let addr: std::net::SocketAddr = addr_str.parse().map_err(|e| {
SdkError::InvalidConfig(format!("invalid QUIC address '{}': {}", addr_str, e))
})?;
Ok(Self { writer: ZiskStreamWriter::quic(addr).map_err(SdkError::backend)? })
}
pub fn grpc() -> Self {
Self { writer: ZiskStreamWriter::push("grpc://push".to_string()) }
}
pub fn write<T: Serialize>(&self, data: &T) {
let bytes = bincode::serde::encode_to_vec(data, bincode::config::standard())
.expect("Failed to serialize");
self.write_slice(&bytes);
}
pub fn write_slice(&self, data: &[u8]) {
let frame = build_frame(data);
self.writer.push_raw(&frame);
}
pub fn write_bytes(&self, data: &[u8]) {
self.writer.push_raw(data);
}
pub fn flush(&self) -> Result<()> {
self.writer.flush().map_err(SdkError::backend)
}
pub fn reset(&self) {
self.writer.reset()
}
pub fn uri(&self) -> &str {
self.writer.uri()
}
pub(crate) fn is_grpc(&self) -> bool {
self.writer.is_push()
}
pub(crate) fn start(&self) -> Result<()> {
self.writer.start().map_err(SdkError::backend)
}
pub(crate) fn set_input_sender(&self, sender: InputSender) {
let adapter = InputSenderPushAdapter::new(sender);
self.writer.set_push_sender(Box::new(adapter));
}
pub fn finish(&self) -> Result<()> {
self.writer.finish().map_err(SdkError::backend)
}
pub(crate) async fn finish_async(&self) -> Result<()> {
let writer = self.writer.clone();
tokio::task::spawn_blocking(move || writer.finish()).await?.map_err(SdkError::backend)
}
}
fn build_frame(data: &[u8]) -> Vec<u8> {
let data_len = data.len();
let total_len = 8 + data_len;
let padding = (8 - (total_len % 8)) % 8;
let mut frame = Vec::with_capacity(total_len + padding);
frame.extend_from_slice(&data_len.to_le_bytes());
frame.extend_from_slice(data);
if padding > 0 {
frame.resize(frame.len() + padding, 0);
}
frame
}
#[cfg(test)]
fn decode_frame(frame: &[u8]) -> Vec<u8> {
assert!(frame.len() >= 8, "frame too short for length header");
let len = usize::from_le_bytes(frame[..8].try_into().unwrap());
frame[8..8 + len].to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use zisk_common::io::{BytesPushSender, StreamError};
fn run_with_timeout<F: FnOnce() + Send + 'static>(name: &str, timeout: Duration, f: F) {
let handle = thread::Builder::new().name(name.into()).spawn(f).unwrap();
let deadline = std::time::Instant::now() + timeout;
loop {
if handle.is_finished() {
handle.join().unwrap();
return;
}
if std::time::Instant::now() > deadline {
panic!("test '{name}' timed out after {timeout:?}");
}
thread::sleep(Duration::from_millis(50));
}
}
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
fn socket_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
struct NoopPushSender;
impl BytesPushSender for NoopPushSender {
fn send_blocking(&self, _data: Vec<u8>) -> Result<(), StreamError> {
Ok(())
}
fn close_blocking(self: Box<Self>) -> Result<(), StreamError> {
Ok(())
}
}
struct RecordingSender(Arc<Mutex<Vec<Vec<u8>>>>);
impl BytesPushSender for RecordingSender {
fn send_blocking(&self, data: Vec<u8>) -> Result<(), StreamError> {
self.0.lock().unwrap().push(data);
Ok(())
}
fn close_blocking(self: Box<Self>) -> Result<(), StreamError> {
Ok(())
}
}
fn force_grpc_ready(stream: &ZiskStream) {
stream.writer.set_push_sender(Box::new(NoopPushSender));
}
#[test]
fn frame_roundtrip_and_alignment() {
let frame = build_frame(b"");
assert_eq!(frame.len(), 8);
assert_eq!(decode_frame(&frame), b"");
let frame = build_frame(b"x");
assert_eq!(frame.len(), 16); assert_eq!(decode_frame(&frame), b"x");
let frame = build_frame(b"hello");
assert_eq!(frame.len(), 16);
assert_eq!(decode_frame(&frame), b"hello");
let frame = build_frame(b"12345678");
assert_eq!(frame.len(), 16);
assert_eq!(decode_frame(&frame), b"12345678");
let data = b"round-trip test data!";
assert_eq!(decode_frame(&build_frame(data)), data.as_slice());
}
#[test]
fn reset_clears_buffered_records() {
let stream = ZiskStream::grpc();
stream.write_slice(b"raw bytes");
stream.write(&42u32);
stream.reset();
force_grpc_ready(&stream);
assert!(stream.flush().is_ok());
}
#[test]
fn flush_empty_is_noop_when_live() {
let stream = ZiskStream::grpc();
force_grpc_ready(&stream);
assert!(stream.flush().is_ok());
}
#[test]
fn quic_rejects_bad_uri() {
assert!(ZiskStream::quic("http://localhost:9000").is_err());
}
#[test]
fn write_unframed_adds_no_length_prefix() {
let stream = ZiskStream::grpc();
let recorded: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
stream.writer.set_push_sender(Box::new(RecordingSender(Arc::clone(&recorded))));
stream.write_slice(b"abcdefgh"); stream.write_bytes(b"01234567");
stream.flush().unwrap();
let chunks = recorded.lock().unwrap();
let received: Vec<u8> = chunks.iter().flatten().copied().collect();
assert_eq!(received.len(), 8 + 8 + 8);
assert_eq!(usize::from_le_bytes(received[..8].try_into().unwrap()), 8);
assert_eq!(&received[8..16], b"abcdefgh");
assert_eq!(&received[16..24], b"01234567");
}
#[cfg(unix)]
mod unix_tests {
use super::*;
use zisk_common::io::{StreamRead, UnixSocketStreamReader};
#[test]
fn unix_write_before_start_then_flush() {
run_with_timeout("unix_write_before_start_then_flush", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let uri = stream.uri().to_string();
let path = uri.strip_prefix("unix://").unwrap().to_string();
stream.write(&42u32);
stream.write(&99u32);
stream.start().unwrap();
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
let msg = reader.next().unwrap().unwrap();
let expected1 =
bincode::serde::encode_to_vec(42u32, bincode::config::standard()).unwrap();
let expected2 =
bincode::serde::encode_to_vec(99u32, bincode::config::standard()).unwrap();
let f1 = build_frame(&expected1);
let f2 = build_frame(&expected2);
let mut concat = f1;
concat.extend_from_slice(&f2);
assert_eq!(msg, concat);
stream.finish().unwrap();
reader.close().unwrap();
});
}
#[test]
fn unix_flush_after_live() {
run_with_timeout("unix_flush_after_live", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.start().unwrap();
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
reader.open().unwrap();
while !stream.writer.is_ready() {
thread::sleep(Duration::from_millis(10));
}
stream.write_slice(b"post-live data");
stream.flush().unwrap();
let msg = reader.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), b"post-live data");
stream.finish().unwrap();
reader.close().unwrap();
});
}
#[test]
fn unix_multiple_flush_cycles() {
run_with_timeout("unix_multiple_flush_cycles", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.start().unwrap();
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
reader.open().unwrap();
while !stream.writer.is_ready() {
thread::sleep(Duration::from_millis(10));
}
stream.write_slice(b"batch-1a");
stream.write_slice(b"batch-1b");
stream.flush().unwrap();
stream.write_slice(b"batch-2");
stream.flush().unwrap();
let m1 = reader.next().unwrap().unwrap();
let m2 = reader.next().unwrap().unwrap();
assert_eq!(decode_frame(&m1[..16]), b"batch-1a");
assert_eq!(decode_frame(&m1[16..32]), b"batch-1b");
assert_eq!(decode_frame(&m2), b"batch-2");
stream.finish().unwrap();
reader.close().unwrap();
});
}
#[test]
fn unix_start_reuse_across_jobs() {
run_with_timeout("unix_start_reuse_across_jobs", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.write(&1u32);
stream.start().unwrap();
let mut reader1 = UnixSocketStreamReader::new(&path).unwrap();
let msg = reader1.next().unwrap().unwrap();
assert_eq!(
decode_frame(&msg),
bincode::serde::encode_to_vec(1u32, bincode::config::standard()).unwrap()
);
stream.finish().unwrap();
reader1.close().unwrap();
stream.write(&2u32);
stream.start().unwrap();
let mut reader2 = UnixSocketStreamReader::new(&path).unwrap();
let msg = reader2.next().unwrap().unwrap();
assert_eq!(
decode_frame(&msg),
bincode::serde::encode_to_vec(2u32, bincode::config::standard()).unwrap()
);
stream.finish().unwrap();
reader2.close().unwrap();
});
}
#[test]
fn unix_finish_makes_stream_not_ready() {
run_with_timeout("unix_finish_makes_stream_not_ready", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.start().unwrap();
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
reader.open().unwrap();
while !stream.writer.is_ready() {
thread::sleep(Duration::from_millis(10));
}
stream.finish().unwrap();
assert!(!stream.writer.is_ready());
reader.close().unwrap();
});
}
#[test]
fn unix_flush_blocks_until_live() {
run_with_timeout("unix_flush_blocks_until_live", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.start().unwrap();
stream.write_slice(b"blocked data");
let stream_clone = stream.clone();
let flushed = Arc::new(AtomicBool::new(false));
let flushed_clone = flushed.clone();
let flush_thread = thread::spawn(move || {
stream_clone.flush().unwrap();
flushed_clone.store(true, Ordering::Release);
});
thread::sleep(Duration::from_millis(100));
assert!(!flushed.load(Ordering::Acquire), "flush should still be blocking");
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
reader.open().unwrap();
flush_thread.join().unwrap();
assert!(flushed.load(Ordering::Acquire));
let msg = reader.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), b"blocked data");
stream.finish().unwrap();
reader.close().unwrap();
});
}
#[test]
fn unix_large_payload() {
run_with_timeout("unix_large_payload", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
let large_data = vec![0xABu8; 64 * 1024]; stream.write_slice(&large_data);
stream.start().unwrap();
let mut reader = UnixSocketStreamReader::new(&path).unwrap();
let msg = reader.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), large_data);
stream.finish().unwrap();
reader.close().unwrap();
});
}
}
mod quic_tests {
use super::*;
use zisk_common::io::{QuicStreamReader, StreamRead};
fn free_port() -> u16 {
std::net::UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
}
#[test]
fn quic_write_before_start_then_read() {
run_with_timeout("quic_write_before_start_then_read", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let port = free_port();
let uri = format!("quic://127.0.0.1:{port}");
let stream = ZiskStream::quic(&uri).unwrap();
stream.write(&42u32);
stream.start().unwrap();
let mut reader =
QuicStreamReader::new(format!("127.0.0.1:{port}").parse().unwrap()).unwrap();
reader.open().unwrap();
let msg = reader.next().unwrap().unwrap();
let expected =
bincode::serde::encode_to_vec(42u32, bincode::config::standard()).unwrap();
assert_eq!(decode_frame(&msg), expected);
stream.finish().unwrap();
reader.close().unwrap();
});
}
#[test]
fn quic_flush_after_live() {
run_with_timeout("quic_flush_after_live", TEST_TIMEOUT, || {
let _g = socket_test_lock();
let port = free_port();
let uri = format!("quic://127.0.0.1:{port}");
let stream = ZiskStream::quic(&uri).unwrap();
stream.start().unwrap();
let mut reader =
QuicStreamReader::new(format!("127.0.0.1:{port}").parse().unwrap()).unwrap();
reader.open().unwrap();
while !stream.writer.is_ready() {
thread::sleep(Duration::from_millis(10));
}
stream.write_slice(b"quic-post-live");
stream.flush().unwrap();
let msg = reader.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), b"quic-post-live");
stream.finish().unwrap();
reader.close().unwrap();
});
}
}
#[test]
fn grpc_reset_then_flush_sends_nothing() {
let stream = ZiskStream::grpc();
stream.write(&1u32);
stream.write(&2u32);
stream.reset();
let recorded: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
stream.writer.set_push_sender(Box::new(RecordingSender(Arc::clone(&recorded))));
stream.flush().unwrap();
assert!(recorded.lock().unwrap().is_empty(), "reset should drop all pending bytes");
}
#[cfg(unix)]
#[test]
fn unix_at_creates_stream_at_explicit_path() {
let path = format!("/tmp/zisk-test-at-{}.sock", uuid::Uuid::new_v4());
let stream = ZiskStream::unix_at(&path).unwrap();
assert_eq!(stream.uri(), format!("unix://{path}"));
assert!(!stream.is_grpc());
}
#[test]
fn finish_without_start_is_ok() {
let stream = ZiskStream::grpc();
assert!(stream.finish().is_ok());
}
#[cfg(unix)]
#[test]
fn finish_without_start_direct() {
let stream = ZiskStream::unix();
assert!(stream.finish().is_ok());
}
#[test]
fn finish_twice_is_idempotent() {
let stream = ZiskStream::grpc();
force_grpc_ready(&stream);
assert!(stream.finish().is_ok());
assert!(stream.finish().is_ok());
assert!(!stream.writer.is_ready());
}
#[cfg(unix)]
#[test]
fn start_while_already_live_tears_down_and_reopens() {
run_with_timeout("start_while_already_live", TEST_TIMEOUT, || {
let _g = socket_test_lock();
use zisk_common::io::{StreamRead, UnixSocketStreamReader};
let stream = ZiskStream::unix();
let path = stream.uri().strip_prefix("unix://").unwrap().to_string();
stream.write_slice(b"job1");
stream.start().unwrap();
let mut reader1 = UnixSocketStreamReader::new(&path).unwrap();
let msg = reader1.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), b"job1");
stream.write_slice(b"job2");
stream.start().unwrap();
let mut reader2 = UnixSocketStreamReader::new(&path).unwrap();
reader2.open().unwrap();
let msg = reader2.next().unwrap().unwrap();
assert_eq!(decode_frame(&msg), b"job2");
stream.finish().unwrap();
reader2.close().unwrap();
});
}
#[test]
fn concurrent_writes_from_clones() {
let stream = ZiskStream::grpc();
let handles: Vec<_> = (0..8)
.map(|i| {
let s = stream.clone();
thread::spawn(move || {
for j in 0..100u32 {
s.write(&(i * 1000 + j));
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let recorded: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
stream.writer.set_push_sender(Box::new(RecordingSender(Arc::clone(&recorded))));
stream.flush().unwrap();
let total_bytes: usize = recorded.lock().unwrap().iter().map(|c| c.len()).sum();
assert_eq!(total_bytes, 800 * 16, "no lost writes — 800 frames × 16 bytes each");
}
}