1use alloc::format;
20use alloc::string::ToString;
21use alloc::sync::Arc;
22use alloc::vec;
23use alloc::vec::Vec;
24use core::time::Duration;
25
26use std::io::{Read, Write};
27use std::net::TcpStream;
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
30use std::thread::JoinHandle;
31use std::time::Instant;
32
33use liminal::protocol::{
34 Frame, ProtocolError, ProtocolVersion, SchemaId, decode, encode, encoded_len,
35};
36
37use crate::SdkError;
38
39const CLIENT_MIN_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
41const CLIENT_MAX_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
43const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
45const READER_POLL_TIMEOUT: Duration = Duration::from_millis(100);
48const SETUP_TIMEOUT: Duration = Duration::from_secs(5);
50const READ_CHUNK_BYTES: usize = 4096;
52const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
54const SUBSCRIPTION_STREAM_ID: u32 = 1;
57const SUBSCRIBE_MAX_IN_FLIGHT: u32 = 1024;
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct DeliveredMessage {
65 delivery_seq: u64,
66 schema_id: SchemaId,
67 payload: Vec<u8>,
68}
69
70impl DeliveredMessage {
71 #[must_use]
74 pub const fn delivery_seq(&self) -> u64 {
75 self.delivery_seq
76 }
77
78 #[must_use]
80 pub const fn schema_id(&self) -> SchemaId {
81 self.schema_id
82 }
83
84 #[must_use]
86 pub fn payload(&self) -> &[u8] {
87 &self.payload
88 }
89
90 #[must_use]
92 pub fn into_payload(self) -> Vec<u8> {
93 self.payload
94 }
95}
96
97#[derive(Debug)]
103pub struct SubscriptionStream {
104 writer: TcpStream,
106 subscription_id: u64,
108 inbound: Receiver<DeliveredMessage>,
110 stop: Arc<AtomicBool>,
112 reader: Option<JoinHandle<()>>,
114}
115
116impl SubscriptionStream {
117 pub fn open(
130 address: &str,
131 channel: &str,
132 accepted_schemas: Vec<SchemaId>,
133 ) -> Result<Self, SdkError> {
134 let mut stream = connect_socket(address)?;
135 let mut buffer = Vec::new();
145 handshake(&mut stream, &mut buffer)?;
146 let subscription_id = subscribe(&mut stream, &mut buffer, channel, accepted_schemas)?;
147
148 let read_stream = stream.try_clone().map_err(|source| SdkError::Protocol {
149 description: format!("failed to clone subscription socket for reader thread: {source}"),
150 })?;
151 let stop = Arc::new(AtomicBool::new(false));
152 let (sender, inbound) = mpsc::channel();
153 let reader_stop = Arc::clone(&stop);
154 let reader = std::thread::Builder::new()
155 .name("liminal-subscription-reader".to_string())
156 .spawn(move || run_reader(read_stream, buffer, &sender, &reader_stop))
157 .map_err(|source| SdkError::Protocol {
158 description: format!("failed to start subscription reader thread: {source}"),
159 })?;
160
161 Ok(Self {
162 writer: stream,
163 subscription_id,
164 inbound,
165 stop,
166 reader: Some(reader),
167 })
168 }
169
170 pub fn recv_timeout(&self, timeout: Duration) -> Result<DeliveredMessage, SdkError> {
177 self.inbound.recv_timeout(timeout).map_err(|error| {
178 let detail = match error {
179 RecvTimeoutError::Timeout => "no delivery arrived within the timeout",
180 RecvTimeoutError::Disconnected => {
181 "the subscription reader stopped before a delivery arrived"
182 }
183 };
184 SdkError::Connection {
185 description: format!("subscription receive failed: {detail}"),
186 }
187 })
188 }
189
190 #[must_use]
192 pub const fn subscription_id(&self) -> u64 {
193 self.subscription_id
194 }
195}
196
197impl Drop for SubscriptionStream {
198 fn drop(&mut self) {
199 self.stop.store(true, Ordering::SeqCst);
200 let unsubscribe = Frame::Unsubscribe {
204 flags: 0,
205 stream_id: SUBSCRIPTION_STREAM_ID,
206 subscription_id: self.subscription_id,
207 };
208 let _ = write_frame(&mut self.writer, &unsubscribe);
209 let _ = write_frame(&mut self.writer, &Frame::Disconnect { flags: 0 });
210 if let Some(reader) = self.reader.take() {
211 reader.join().ok();
214 }
215 }
216}
217
218fn connect_socket(address: &str) -> Result<TcpStream, SdkError> {
221 let stream = TcpStream::connect(address).map_err(|source| SdkError::Connection {
222 description: format!("failed to connect subscription client to {address}: {source}"),
223 })?;
224 stream
225 .set_nodelay(true)
226 .map_err(|source| SdkError::Connection {
227 description: format!("failed to disable Nagle for {address}: {source}"),
228 })?;
229 stream
230 .set_read_timeout(Some(READER_POLL_TIMEOUT))
231 .map_err(|source| SdkError::Connection {
232 description: format!("failed to set subscription read timeout for {address}: {source}"),
233 })?;
234 stream
235 .set_write_timeout(Some(WRITE_TIMEOUT))
236 .map_err(|source| SdkError::Connection {
237 description: format!(
238 "failed to set subscription write timeout for {address}: {source}"
239 ),
240 })?;
241 Ok(stream)
242}
243
244fn handshake(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<(), SdkError> {
249 let connect = Frame::Connect {
250 flags: 0,
251 min_version: CLIENT_MIN_VERSION,
252 max_version: CLIENT_MAX_VERSION,
253 auth_token: Vec::new(),
254 };
255 write_frame(stream, &connect)?;
256 match read_one_frame(stream, buffer)? {
257 Frame::ConnectAck { .. } => Ok(()),
258 Frame::ConnectError {
259 reason_code,
260 message,
261 ..
262 } => Err(SdkError::Connection {
263 description: format!(
264 "server rejected subscription connection (reason {reason_code}): {}",
265 message.unwrap_or_else(|| "no detail".to_string())
266 ),
267 }),
268 other => Err(SdkError::Protocol {
269 description: format!(
270 "expected ConnectAck during subscription handshake, received {:?}",
271 other.frame_type()
272 ),
273 }),
274 }
275}
276
277fn subscribe(
280 stream: &mut TcpStream,
281 buffer: &mut Vec<u8>,
282 channel: &str,
283 accepted_schemas: Vec<SchemaId>,
284) -> Result<u64, SdkError> {
285 let frame = Frame::Subscribe {
286 flags: 0,
287 stream_id: SUBSCRIPTION_STREAM_ID,
288 channel: channel.to_string(),
289 accepted_schemas,
290 max_in_flight: SUBSCRIBE_MAX_IN_FLIGHT,
291 };
292 write_frame(stream, &frame)?;
293 match read_one_frame(stream, buffer)? {
294 Frame::SubscribeAck {
295 subscription_id, ..
296 } => Ok(subscription_id),
297 Frame::SubscribeError {
298 reason_code,
299 message,
300 ..
301 } => Err(SdkError::Protocol {
302 description: format!(
303 "server rejected subscribe (reason {reason_code}): {}",
304 message.unwrap_or_else(|| "no detail".to_string())
305 ),
306 }),
307 other => Err(SdkError::Protocol {
308 description: format!(
309 "expected SubscribeAck during subscribe, received {:?}",
310 other.frame_type()
311 ),
312 }),
313 }
314}
315
316fn run_reader(
328 mut stream: TcpStream,
329 mut buffer: Vec<u8>,
330 sender: &Sender<DeliveredMessage>,
331 stop: &AtomicBool,
332) {
333 while !stop.load(Ordering::SeqCst) {
334 let frame = match next_frame(&mut stream, &mut buffer) {
335 Ok(Some(frame)) => frame,
336 Ok(None) => continue,
338 Err(_) => return,
341 };
342 match frame {
343 Frame::Deliver {
344 delivery_seq,
345 envelope,
346 ..
347 } => {
348 let message = DeliveredMessage {
349 delivery_seq,
350 schema_id: envelope.schema_id,
351 payload: envelope.payload,
352 };
353 if sender.send(message).is_err() {
354 return;
357 }
358 }
359 Frame::Disconnect { .. } => return,
361 _ => {}
365 }
366 }
367}
368
369fn next_frame(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<Option<Frame>, SdkError> {
372 loop {
373 match decode(buffer) {
374 Ok((frame, consumed)) => {
375 buffer.drain(..consumed);
376 return Ok(Some(frame));
377 }
378 Err(
379 ProtocolError::IncompleteHeader { .. } | ProtocolError::TruncatedPayload { .. },
380 ) => match fill_buffer(stream, buffer)? {
381 FillOutcome::Read => {}
382 FillOutcome::TimedOut => return Ok(None),
383 },
384 Err(error) => return Err(protocol_error(&error)),
385 }
386 }
387}
388
389fn read_one_frame(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<Frame, SdkError> {
393 let deadline = Instant::now() + SETUP_TIMEOUT;
394 loop {
395 match decode(buffer) {
396 Ok((frame, consumed)) => {
397 buffer.drain(..consumed);
398 return Ok(frame);
399 }
400 Err(
401 ProtocolError::IncompleteHeader { .. } | ProtocolError::TruncatedPayload { .. },
402 ) => match fill_buffer(stream, buffer)? {
403 FillOutcome::Read => {}
404 FillOutcome::TimedOut => {
405 if Instant::now() >= deadline {
406 return Err(SdkError::Connection {
407 description:
408 "subscription connection timed out waiting for a control-frame reply"
409 .to_string(),
410 });
411 }
412 }
413 },
414 Err(error) => return Err(protocol_error(&error)),
415 }
416 }
417}
418
419fn fill_buffer(stream: &mut TcpStream, buffer: &mut Vec<u8>) -> Result<FillOutcome, SdkError> {
422 if buffer.len() > MAX_FRAME_BYTES {
423 return Err(SdkError::Protocol {
424 description: format!(
425 "subscription frame exceeded {MAX_FRAME_BYTES} bytes without a complete frame"
426 ),
427 });
428 }
429 let mut chunk = [0_u8; READ_CHUNK_BYTES];
430 match stream.read(&mut chunk) {
431 Ok(0) => Err(SdkError::Connection {
432 description: "server closed the subscription connection".to_string(),
433 }),
434 Ok(read) => {
435 let Some(received) = chunk.get(..read) else {
436 return Err(SdkError::Protocol {
437 description:
438 "subscription socket read reported more bytes than the buffer holds"
439 .to_string(),
440 });
441 };
442 buffer.extend_from_slice(received);
443 Ok(FillOutcome::Read)
444 }
445 Err(error)
446 if matches!(
447 error.kind(),
448 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
449 ) =>
450 {
451 Ok(FillOutcome::TimedOut)
452 }
453 Err(error) => Err(SdkError::Connection {
454 description: format!("failed to read from subscription connection: {error}"),
455 }),
456 }
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461enum FillOutcome {
462 Read,
463 TimedOut,
464}
465
466fn write_frame(stream: &mut TcpStream, frame: &Frame) -> Result<(), SdkError> {
468 let len = encoded_len(frame).map_err(|error| protocol_error(&error))?;
469 let mut bytes = vec![0_u8; len];
470 let written = encode(frame, &mut bytes).map_err(|error| protocol_error(&error))?;
471 let encoded = bytes.get(..written).ok_or_else(|| SdkError::Protocol {
472 description: "subscription wire encoder reported an invalid byte count".to_string(),
473 })?;
474 stream
475 .write_all(encoded)
476 .map_err(|source| SdkError::Connection {
477 description: format!("failed to write subscription frame: {source}"),
478 })?;
479 stream.flush().map_err(|source| SdkError::Connection {
480 description: format!("failed to flush subscription frame: {source}"),
481 })
482}
483
484fn protocol_error(error: &ProtocolError) -> SdkError {
486 SdkError::Protocol {
487 description: format!("subscription wire codec error: {error}"),
488 }
489}
490
491#[cfg(test)]
492#[path = "subscription_tests.rs"]
493mod tests;