1mod flush;
26pub(in crate::remote) mod participant;
30mod pump_diagnostics;
31mod push_client;
32mod subscription;
33
34pub use flush::{FlushMode, FlushOutcome, PublishRejection};
35#[cfg(test)]
36pub(in crate::remote) use push_client::connect_socket_for_tests as connect_push_socket_for_tests;
37pub use push_client::{
38 OBSERVABILITY_CHANNEL, PendingPushConnect, PushClient, PushCloseCause, PushReceiveOutcome,
39 PushReceiver, PushWriter, PushedFrame,
40};
41pub use subscription::{DeliveredMessage, SubscriptionStream};
42
43use alloc::format;
44use alloc::string::{String, ToString};
45use alloc::sync::Arc;
46use alloc::vec::Vec;
47use core::fmt;
48use core::time::Duration;
49use std::net::TcpStream;
50
51use liminal::protocol::{
52 CausalContext, Frame, MessageEnvelope, PUBLISH_IDEMPOTENCY_KEY_FLAG, ProtocolVersion, SchemaId,
53};
54use std::sync::{Mutex, MutexGuard, PoisonError};
55
56use crate::{DeliveryAck, PressureResponse, SdkError};
57
58use super::ServerAddress;
59use super::framing::{Connection, unexpected_frame, write_frame};
60use super::participant::ParticipantResponseProvenance;
61use super::protocol::{
62 ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
63 WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
64};
65use super::publish_reply;
66
67const APPLICATION_STREAM_ID: u32 = 1;
69const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
71const SCHEMALESS_SCHEMA: &[u8] = &[];
73
74struct WriterSlot {
77 stream: TcpStream,
78 provenance: ParticipantResponseProvenance,
79}
80
81fn held<T>(guard: &Mutex<T>) -> MutexGuard<'_, T> {
85 guard.lock().unwrap_or_else(PoisonError::into_inner)
86}
87
88struct ConnectionSlot {
89 connection: Connection<TcpStream>,
90 provenance: ParticipantResponseProvenance,
91 next_attempt_id: u64,
92 next_connection_id: u64,
93}
94
95pub struct TcpRemoteTransport {
97 connection: Arc<Mutex<ConnectionSlot>>,
98 writer: Arc<Mutex<WriterSlot>>,
99 #[cfg(unix)]
100 read_wake: super::ReadWake,
101 address: String,
102 auth_token: Vec<u8>,
103}
104
105impl fmt::Debug for TcpRemoteTransport {
106 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107 formatter
108 .debug_struct("TcpRemoteTransport")
109 .finish_non_exhaustive()
110 }
111}
112
113impl TcpRemoteTransport {
114 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
122 Self::connect_with_auth(server_address, &[])
123 }
124
125 pub fn connect_with_auth(
137 server_address: &ServerAddress,
138 auth_token: &[u8],
139 ) -> Result<Self, SdkError> {
140 let address = server_address.as_str().to_string();
141 let connection = Connection::connect_with_auth(&address, auth_token)?;
142 #[cfg(unix)]
143 let read_wake = super::ReadWake::new()?;
144 #[cfg(unix)]
145 let connection = {
146 let mut connection = connection;
147 connection.wake_idle_reads_through(read_wake.clone());
148 connection
149 };
150 let provenance = ParticipantResponseProvenance::new(1, 1);
151 let writer = WriterSlot {
152 stream: connection.write_half()?,
153 provenance,
154 };
155 Ok(Self {
156 #[cfg(unix)]
157 read_wake,
158 writer: Arc::new(Mutex::new(writer)),
159 connection: Arc::new(Mutex::new(ConnectionSlot {
160 connection,
161 provenance,
162 next_attempt_id: 2,
163 next_connection_id: 2,
164 })),
165 address,
166 auth_token: auth_token.to_vec(),
167 })
168 }
169
170 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
171 let (mut slot, _writer) = self.exclusive();
172 slot.connection.round_trip(request)
173 }
174
175 fn exclusive(&self) -> (MutexGuard<'_, ConnectionSlot>, MutexGuard<'_, WriterSlot>) {
178 let slot = held(&self.connection);
179 let writer = held(&self.writer);
180 (slot, writer)
181 }
182}
183
184impl ParticipantRemoteTransport for TcpRemoteTransport {
185 fn send_participant(
186 &self,
187 _server_address: &ServerAddress,
188 request: &liminal_protocol::wire::ClientRequest,
189 ) -> Result<ParticipantResponseProvenance, SdkError> {
190 let mut writer = held(&self.writer);
191 write_frame(&mut writer.stream, &participant::request_frame(request)?)?;
192 Ok(writer.provenance)
193 }
194
195 fn receive_participant(
196 &self,
197 _server_address: &ServerAddress,
198 ) -> Result<ParticipantTransportFrame, SdkError> {
199 let mut slot = held(&self.connection);
200 let frame = slot.connection.receive_participant()?;
201 Ok(ParticipantTransportFrame {
202 frame,
203 provenance: slot.provenance,
204 })
205 }
206
207 fn receive_participant_within(
208 &self,
209 _server_address: &ServerAddress,
210 budget: Duration,
211 ) -> Result<Option<ParticipantTransportFrame>, SdkError> {
212 let mut slot = held(&self.connection);
213 let Some(frame) = slot.connection.receive_participant_within(budget)? else {
214 return Ok(None);
215 };
216 Ok(Some(ParticipantTransportFrame {
217 frame,
218 provenance: slot.provenance,
219 }))
220 }
221
222 fn try_receive_participant(&self) -> Result<Option<ParticipantTransportFrame>, SdkError> {
223 let (frame, provenance) = {
224 let mut slot = held(&self.connection);
225 (slot.connection.try_receive_participant()?, slot.provenance)
226 };
227 Ok(frame.map(|frame| ParticipantTransportFrame { frame, provenance }))
228 }
229
230 fn reconnect_participant(
231 &self,
232 _server_address: &ServerAddress,
233 ) -> Result<ParticipantResponseProvenance, SdkError> {
234 let (mut slot, mut writer) = self.exclusive();
235 let attempt_id = slot.next_attempt_id;
236 slot.next_attempt_id =
237 slot.next_attempt_id
238 .checked_add(1)
239 .ok_or_else(|| SdkError::Connection {
240 description: "participant transport attempt identity exhausted".to_string(),
241 })?;
242 let connection = Connection::connect_with_auth(&self.address, &self.auth_token)?;
243 #[cfg(unix)]
244 let connection = {
245 let mut connection = connection;
246 connection.wake_idle_reads_through(self.read_wake.clone());
247 connection
248 };
249 let connection_id = slot.next_connection_id;
250 slot.next_connection_id =
251 slot.next_connection_id
252 .checked_add(1)
253 .ok_or_else(|| SdkError::Connection {
254 description: "participant transport connection identity exhausted".to_string(),
255 })?;
256 let provenance = ParticipantResponseProvenance::new(connection_id, attempt_id);
257 writer.stream = connection.write_half()?;
258 writer.provenance = provenance;
259 slot.connection = connection;
260 slot.provenance = provenance;
261 Ok(provenance)
262 }
263}
264
265impl RemoteTransport for TcpRemoteTransport {
266 #[cfg(unix)]
267 fn read_wake(&self) -> Option<super::ReadWake> {
268 Some(self.read_wake.clone())
269 }
270
271 fn publish(
272 &self,
273 _server_address: &ServerAddress,
274 request: &WirePublishRequest,
275 ) -> Result<PressureResponse, SdkError> {
276 let frame = build_publish_frame(request);
277 let response = self.round_trip(&frame)?;
278 publish_response(response)
279 }
280
281 fn publish_with_delivery(
282 &self,
283 _server_address: &ServerAddress,
284 request: &WirePublishRequest,
285 ) -> Result<DeliveryAck, SdkError> {
286 let frame = build_publish_frame(request);
287 let response = self.round_trip(&frame)?;
288 publish_delivery_response(response)
289 }
290
291 fn subscribe(
312 &self,
313 _server_address: &ServerAddress,
314 request: &WireSubscribeRequest,
315 ) -> Result<(), SdkError> {
316 let frame = Frame::Subscribe {
317 flags: 0,
318 stream_id: request.stream_id(),
319 channel: request.channel().to_string(),
320 accepted_schemas: Vec::new(),
323 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
324 };
325 let response = self.round_trip(&frame)?;
326 subscribe_response(response)
327 }
328
329 fn send_conversation(
330 &self,
331 _server_address: &ServerAddress,
332 request: &WireConversationRequest,
333 ) -> Result<(), SdkError> {
334 let conversation_label = request.conversation_id().as_str();
335 let conversation_id = conversation_wire_id(conversation_label);
336 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
337 let (mut slot, _writer) = self.exclusive();
338 slot.connection
339 .send_conversation_message(conversation_id, conversation_label, envelope)
340 }
341
342 fn request_reply_conversation(
343 &self,
344 _server_address: &ServerAddress,
345 request: &WireConversationRequest,
346 ) -> Result<Vec<u8>, SdkError> {
347 let conversation_label = request.conversation_id().as_str();
348 let conversation_id = conversation_wire_id(conversation_label);
349 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
350 let (mut slot, _writer) = self.exclusive();
351 slot.connection
352 .conversation_request_reply(conversation_id, conversation_label, envelope)
353 }
354
355 fn resume(
356 &self,
357 _server_address: &ServerAddress,
358 request: &WireResumeRequest,
359 ) -> Result<(), SdkError> {
360 let _ = (request.subscription_id(), request.resume_from_sequence());
370 Err(SdkError::Protocol {
371 description:
372 "resume is not yet supported over the TCP transport; re-subscribe for live \
373 delivery only; messages published while disconnected are not replayed"
374 .to_string(),
375 })
376 }
377
378 fn negotiated_version(&self) -> Option<ProtocolVersion> {
379 held(&self.connection).connection.negotiated_version()
380 }
381}
382
383fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
384 MessageEnvelope::new(
385 schema_id_from_bytes(schema_bytes),
386 CausalContext::independent(),
387 payload.to_vec(),
388 )
389}
390
391fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
397 SchemaId::from_schema_bytes(schema_bytes)
398}
399
400fn conversation_wire_id(conversation_id: &str) -> u64 {
401 fnv1a(conversation_id.as_bytes())
402}
403
404fn fnv1a(bytes: &[u8]) -> u64 {
406 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
407 const PRIME: u64 = 0x0000_0100_0000_01b3;
408 let mut hash = OFFSET_BASIS;
409 for byte in bytes {
410 hash ^= u64::from(*byte);
411 hash = hash.wrapping_mul(PRIME);
412 }
413 hash
414}
415
416fn build_publish_frame(request: &WirePublishRequest) -> Frame {
420 let mut envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
421 envelope.causal_context = request.causal_context();
422 let flags = match request.idempotency_key() {
423 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
424 None => 0,
425 };
426 Frame::Publish {
427 flags,
428 stream_id: APPLICATION_STREAM_ID,
429 channel: request.channel().to_string(),
430 envelope,
431 idempotency_key: request.idempotency_key().map(ToString::to_string),
432 }
433}
434
435fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
436 match frame {
437 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
438 Frame::PublishError {
439 reason_code,
440 message,
441 ..
442 } => Err(publish_reply::publish_error(reason_code, message)),
443 other => Err(unexpected_frame("PublishAck", &other)),
444 }
445}
446
447fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
451 match frame {
452 Frame::PublishAck {
453 flags,
454 durable_position,
455 ..
456 } => Ok(publish_reply::delivery_ack(flags, durable_position)),
457 Frame::PublishError {
458 reason_code,
459 message,
460 ..
461 } => Err(publish_reply::publish_error(reason_code, message)),
462 other => Err(unexpected_frame("PublishAck", &other)),
463 }
464}
465
466fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
467 match frame {
468 Frame::SubscribeAck { .. } => Ok(()),
469 Frame::SubscribeError {
470 reason_code,
471 message,
472 ..
473 } => Err(SdkError::Protocol {
474 description: format!(
475 "server rejected subscribe (reason {reason_code}): {}",
476 message.unwrap_or_else(|| "no detail".to_string())
477 ),
478 }),
479 other => Err(unexpected_frame("SubscribeAck", &other)),
480 }
481}
482
483#[cfg(all(test, unix))]
484#[path = "concurrency_tests.rs"]
485mod concurrency_tests;
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn schema_ids_are_deterministic_and_distinct() {
493 assert_eq!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"a"));
494 assert_ne!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"b"));
495 }
496
497 #[test]
498 fn conversation_ids_are_stable() {
499 assert_eq!(conversation_wire_id("chat"), conversation_wire_id("chat"));
500 assert_ne!(conversation_wire_id("chat"), conversation_wire_id("other"));
501 }
502
503 #[test]
504 fn publish_ack_maps_to_accept() -> Result<(), SdkError> {
505 let frame = Frame::PublishAck {
506 flags: 0,
507 stream_id: 1,
508 message_id: 7,
509 durable_position: None,
510 };
511 assert_eq!(publish_response(frame)?, PressureResponse::Accept);
512 Ok(())
513 }
514
515 #[test]
516 fn publish_ack_with_position_maps_to_accept() -> Result<(), SdkError> {
517 let frame = Frame::PublishAck {
518 flags: 0x02,
519 stream_id: 1,
520 message_id: 7,
521 durable_position: Some(7717),
522 };
523 assert_eq!(publish_response(frame.clone())?, PressureResponse::Accept);
524 let ack = publish_delivery_response(frame)?;
525 assert!(
526 !ack.is_accepted(),
527 "the position bit is not the delivered bit"
528 );
529 assert_eq!(ack.durable_position(), Some(7717));
530 Ok(())
531 }
532
533 #[test]
534 fn publish_unresolved_maps_to_publish_unresolved() {
535 let frame = Frame::PublishError {
536 flags: 0,
537 stream_id: 1,
538 reason_code: 0x0103,
539 message: Some("orders k1".into()),
540 };
541 assert!(matches!(
542 publish_response(frame),
543 Err(SdkError::PublishUnresolved { description }) if description == "orders k1"
544 ));
545 }
546
547 #[test]
548 fn publish_error_maps_to_backpressure() {
549 let frame = Frame::PublishError {
550 flags: 0,
551 stream_id: 1,
552 reason_code: 9,
553 message: Some("nope".to_string()),
554 };
555 assert!(matches!(
556 publish_response(frame),
557 Err(SdkError::Backpressure { .. })
558 ));
559 }
560}