use alloc::format;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::time::Duration;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::thread::JoinHandle;
use std::time::Instant;
use liminal::protocol::{
Frame, ProtocolError, ProtocolVersion, SchemaId, decode, encode, encoded_len,
};
use crate::SdkError;
const CLIENT_MIN_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
const CLIENT_MAX_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
const READER_POLL_TIMEOUT: Duration = Duration::from_millis(100);
const SETUP_TIMEOUT: Duration = Duration::from_secs(5);
const READ_CHUNK_BYTES: usize = 4096;
const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
const SUBSCRIPTION_STREAM_ID: u32 = 1;
const SUBSCRIBE_MAX_IN_FLIGHT: u32 = 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeliveredMessage {
delivery_seq: u64,
schema_id: SchemaId,
payload: Vec<u8>,
}
impl DeliveredMessage {
#[must_use]
pub const fn delivery_seq(&self) -> u64 {
self.delivery_seq
}
#[must_use]
pub const fn schema_id(&self) -> SchemaId {
self.schema_id
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
#[must_use]
pub fn into_payload(self) -> Vec<u8> {
self.payload
}
}
#[derive(Debug)]
pub struct SubscriptionStream {
writer: TcpStream,
subscription_id: u64,
inbound: Receiver<DeliveredMessage>,
stop: Arc<AtomicBool>,
reader: Option<JoinHandle<()>>,
}
impl SubscriptionStream {
pub fn open(
address: &str,
channel: &str,
accepted_schemas: Vec<SchemaId>,
) -> Result<Self, SdkError> {
let mut stream = connect_socket(address)?;
let mut buffer = Vec::new();
handshake(&mut stream, &mut buffer)?;
let subscription_id = subscribe(&mut stream, &mut buffer, channel, accepted_schemas)?;
let read_stream = stream.try_clone().map_err(|source| SdkError::Protocol {
description: format!("failed to clone subscription socket for reader thread: {source}"),
})?;
let stop = Arc::new(AtomicBool::new(false));
let (sender, inbound) = mpsc::channel();
let reader_stop = Arc::clone(&stop);
let reader = std::thread::Builder::new()
.name("liminal-subscription-reader".to_string())
.spawn(move || run_reader(read_stream, buffer, &sender, &reader_stop))
.map_err(|source| SdkError::Protocol {
description: format!("failed to start subscription reader thread: {source}"),
})?;
Ok(Self {
writer: stream,
subscription_id,
inbound,
stop,
reader: Some(reader),
})
}
pub fn recv_timeout(&self, timeout: Duration) -> Result<DeliveredMessage, SdkError> {
self.inbound.recv_timeout(timeout).map_err(|error| {
let detail = match error {
RecvTimeoutError::Timeout => "no delivery arrived within the timeout",
RecvTimeoutError::Disconnected => {
"the subscription reader stopped before a delivery arrived"
}
};
SdkError::Connection {
description: format!("subscription receive failed: {detail}"),
}
})
}
#[must_use]
pub const fn subscription_id(&self) -> u64 {
self.subscription_id
}
}
impl Drop for SubscriptionStream {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
let unsubscribe = Frame::Unsubscribe {
flags: 0,
stream_id: SUBSCRIPTION_STREAM_ID,
subscription_id: self.subscription_id,
};
let _ = write_frame(&mut self.writer, &unsubscribe);
let _ = write_frame(&mut self.writer, &Frame::Disconnect { flags: 0 });
if let Some(reader) = self.reader.take() {
reader.join().ok();
}
}
}
fn connect_socket(address: &str) -> Result<TcpStream, SdkError> {
let stream = TcpStream::connect(address).map_err(|source| SdkError::Connection {
description: format!("failed to connect subscription client to {address}: {source}"),
})?;
stream
.set_nodelay(true)
.map_err(|source| SdkError::Connection {
description: format!("failed to disable Nagle for {address}: {source}"),
})?;
stream
.set_read_timeout(Some(READER_POLL_TIMEOUT))
.map_err(|source| SdkError::Connection {
description: format!("failed to set subscription read timeout for {address}: {source}"),
})?;
stream
.set_write_timeout(Some(WRITE_TIMEOUT))
.map_err(|source| SdkError::Connection {
description: format!(
"failed to set subscription write timeout for {address}: {source}"
),
})?;
Ok(stream)
}
fn handshake(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<(), SdkError> {
let connect = Frame::Connect {
flags: 0,
min_version: CLIENT_MIN_VERSION,
max_version: CLIENT_MAX_VERSION,
auth_token: Vec::new(),
};
write_frame(stream, &connect)?;
match read_one_frame(stream, buffer)? {
Frame::ConnectAck { .. } => Ok(()),
Frame::ConnectError {
reason_code,
message,
..
} => Err(SdkError::Connection {
description: format!(
"server rejected subscription connection (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}),
other => Err(SdkError::Protocol {
description: format!(
"expected ConnectAck during subscription handshake, received {:?}",
other.frame_type()
),
}),
}
}
fn subscribe(
stream: &mut TcpStream,
buffer: &mut Vec<u8>,
channel: &str,
accepted_schemas: Vec<SchemaId>,
) -> Result<u64, SdkError> {
let frame = Frame::Subscribe {
flags: 0,
stream_id: SUBSCRIPTION_STREAM_ID,
channel: channel.to_string(),
accepted_schemas,
max_in_flight: SUBSCRIBE_MAX_IN_FLIGHT,
};
write_frame(stream, &frame)?;
match read_one_frame(stream, buffer)? {
Frame::SubscribeAck {
subscription_id, ..
} => Ok(subscription_id),
Frame::SubscribeError {
reason_code,
message,
..
} => Err(SdkError::Protocol {
description: format!(
"server rejected subscribe (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}),
other => Err(SdkError::Protocol {
description: format!(
"expected SubscribeAck during subscribe, received {:?}",
other.frame_type()
),
}),
}
}
fn run_reader(
mut stream: TcpStream,
mut buffer: Vec<u8>,
sender: &Sender<DeliveredMessage>,
stop: &AtomicBool,
) {
while !stop.load(Ordering::SeqCst) {
let frame = match next_frame(&mut stream, &mut buffer) {
Ok(Some(frame)) => frame,
Ok(None) => continue,
Err(_) => return,
};
match frame {
Frame::Deliver {
delivery_seq,
envelope,
..
} => {
let message = DeliveredMessage {
delivery_seq,
schema_id: envelope.schema_id,
payload: envelope.payload,
};
if sender.send(message).is_err() {
return;
}
}
Frame::Disconnect { .. } => return,
_ => {}
}
}
}
fn next_frame(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<Option<Frame>, SdkError> {
loop {
match decode(buffer) {
Ok((frame, consumed)) => {
buffer.drain(..consumed);
return Ok(Some(frame));
}
Err(
ProtocolError::IncompleteHeader { .. } | ProtocolError::TruncatedPayload { .. },
) => match fill_buffer(stream, buffer)? {
FillOutcome::Read => {}
FillOutcome::TimedOut => return Ok(None),
},
Err(error) => return Err(protocol_error(&error)),
}
}
}
fn read_one_frame(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<Frame, SdkError> {
let deadline = Instant::now() + SETUP_TIMEOUT;
loop {
match decode(buffer) {
Ok((frame, consumed)) => {
buffer.drain(..consumed);
return Ok(frame);
}
Err(
ProtocolError::IncompleteHeader { .. } | ProtocolError::TruncatedPayload { .. },
) => match fill_buffer(stream, buffer)? {
FillOutcome::Read => {}
FillOutcome::TimedOut => {
if Instant::now() >= deadline {
return Err(SdkError::Connection {
description:
"subscription connection timed out waiting for a control-frame reply"
.to_string(),
});
}
}
},
Err(error) => return Err(protocol_error(&error)),
}
}
}
fn fill_buffer(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<FillOutcome, SdkError> {
if buffer.len() > MAX_FRAME_BYTES {
return Err(SdkError::Protocol {
description: format!(
"subscription frame exceeded {MAX_FRAME_BYTES} bytes without a complete frame"
),
});
}
let mut chunk = [0_u8; READ_CHUNK_BYTES];
match stream.read(&mut chunk) {
Ok(0) => Err(SdkError::Connection {
description: "server closed the subscription connection".to_string(),
}),
Ok(read) => {
let Some(received) = chunk.get(..read) else {
return Err(SdkError::Protocol {
description:
"subscription socket read reported more bytes than the buffer holds"
.to_string(),
});
};
buffer.extend_from_slice(received);
Ok(FillOutcome::Read)
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
Ok(FillOutcome::TimedOut)
}
Err(error) => Err(SdkError::Connection {
description: format!("failed to read from subscription connection: {error}"),
}),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FillOutcome {
Read,
TimedOut,
}
fn write_frame(stream: &mut TcpStream, frame: &Frame) -> Result<(), SdkError> {
let len = encoded_len(frame).map_err(|error| protocol_error(&error))?;
let mut bytes = vec![0_u8; len];
let written = encode(frame, &mut bytes).map_err(|error| protocol_error(&error))?;
let encoded = bytes.get(..written).ok_or_else(|| SdkError::Protocol {
description: "subscription wire encoder reported an invalid byte count".to_string(),
})?;
stream
.write_all(encoded)
.map_err(|source| SdkError::Connection {
description: format!("failed to write subscription frame: {source}"),
})?;
stream.flush().map_err(|source| SdkError::Connection {
description: format!("failed to flush subscription frame: {source}"),
})
}
fn protocol_error(error: &ProtocolError) -> SdkError {
SdkError::Protocol {
description: format!("subscription wire codec error: {error}"),
}
}
#[cfg(test)]
#[path = "subscription_tests.rs"]
mod tests;