1use std::{
33 collections::VecDeque,
34 fmt::Debug,
35 path::Path,
36 pin::pin,
37 sync::{
38 Arc,
39 atomic::{AtomicU8, Ordering},
40 },
41 time::Duration,
42};
43
44use bytes::Bytes;
45use nautilus_core::CleanDrop;
46use nautilus_cryptography::providers::install_cryptographic_provider;
47use tokio::io::{AsyncReadExt, AsyncWriteExt};
48use tokio_tungstenite::tungstenite::{Error, client::IntoClientRequest, stream::Mode};
49
50use super::{SocketConfig, TcpMessageHandler, TcpReader, TcpWriter, WriterCommand};
51use crate::{
52 backoff::ExponentialBackoff,
53 dst,
54 error::{SendError, is_connection_drop_io_error},
55 logging::{log_task_aborted, log_task_started, log_task_stopped},
56 mode::ConnectionMode,
57 net::TcpStream,
58 tls::{Connector, create_tls_config_from_certs_dir, tcp_tls},
59};
60
61const CONNECTION_STATE_CHECK_INTERVAL_MS: u64 = 10;
63const GRACEFUL_SHUTDOWN_DELAY_MS: u64 = 100;
64const GRACEFUL_SHUTDOWN_TIMEOUT_SECS: u64 = 5;
65
66const MAX_READ_BUFFER_BYTES: usize = 10 * 1024 * 1024;
68
69#[cfg_attr(
85 feature = "python",
86 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network")
87)]
88struct SocketClientInner {
89 config: SocketConfig,
90 connector: Option<Connector>,
91 read_task: Arc<tokio::task::JoinHandle<()>>,
92 write_task: tokio::task::JoinHandle<()>,
93 writer_tx: tokio::sync::mpsc::UnboundedSender<WriterCommand>,
94 heartbeat_task: Option<tokio::task::JoinHandle<()>>,
95 connection_mode: Arc<AtomicU8>,
96 state_notify: Arc<tokio::sync::Notify>,
97 reconnect_timeout: Duration,
98 backoff: ExponentialBackoff,
99 handler: Option<TcpMessageHandler>,
100 reconnect_max_attempts: Option<u32>,
101 reconnect_attempt_count: u32,
102}
103
104impl SocketClientInner {
105 pub(crate) async fn connect_url(config: SocketConfig) -> anyhow::Result<Self> {
111 const CONNECTION_TIMEOUT_SECS: u64 = 10;
112
113 install_cryptographic_provider();
114
115 if config.suffix.is_empty() {
117 anyhow::bail!("Socket suffix cannot be empty: suffix is required for message framing");
118 }
119
120 if let Some((interval_secs, _)) = &config.heartbeat
121 && *interval_secs == 0
122 {
123 anyhow::bail!("Heartbeat interval cannot be zero");
124 }
125
126 if config.idle_timeout_ms == Some(0) {
127 anyhow::bail!("Idle timeout cannot be zero");
128 }
129
130 let SocketConfig {
131 url,
132 mode,
133 heartbeat,
134 suffix,
135 message_handler,
136 reconnect_timeout_ms,
137 reconnect_delay_initial_ms,
138 reconnect_delay_max_ms,
139 reconnect_backoff_factor,
140 reconnect_jitter_ms,
141 connection_max_retries,
142 reconnect_max_attempts,
143 idle_timeout_ms,
144 certs_dir,
145 } = &config.clone();
146 let connector = if let Some(dir) = certs_dir {
147 let config = create_tls_config_from_certs_dir(Path::new(dir), false)?;
148 Some(Connector::Rustls(Arc::new(config)))
149 } else {
150 None
151 };
152
153 let max_retries = connection_max_retries.unwrap_or(5);
155
156 let mut backoff = ExponentialBackoff::new(
157 Duration::from_millis(500),
158 Duration::from_secs(5),
159 2.0,
160 250,
161 false,
162 )?;
163
164 #[allow(unused_assignments)]
165 let mut last_error = String::new();
166 let mut attempt = 0;
167 let (reader, writer) = loop {
168 attempt += 1;
169
170 match dst::time::timeout(
171 Duration::from_secs(CONNECTION_TIMEOUT_SECS),
172 Self::tls_connect_with_server(url, *mode, connector.clone()),
173 )
174 .await
175 {
176 Ok(Ok(result)) => {
177 if attempt > 1 {
178 log::info!("Socket connection established after {attempt} attempts");
179 }
180 break result;
181 }
182 Ok(Err(e)) => {
183 last_error = e.to_string();
184 log::warn!(
185 "Socket connection attempt {attempt}/{max_retries} to {url} failed: {last_error}"
186 );
187 }
188 Err(_) => {
189 last_error = format!(
190 "Connection timeout after {CONNECTION_TIMEOUT_SECS}s (possible DNS resolution failure)"
191 );
192 log::warn!(
193 "Socket connection attempt {attempt}/{max_retries} to {url} timed out"
194 );
195 }
196 }
197
198 if attempt >= max_retries {
199 anyhow::bail!(
200 "Failed to connect to {} after {} attempts: {}. \
201 If this is a DNS error, check your network configuration and DNS settings.",
202 url,
203 max_retries,
204 if last_error.is_empty() {
205 "unknown error"
206 } else {
207 &last_error
208 }
209 );
210 }
211
212 let delay = backoff.next_duration();
213 log::debug!(
214 "Retrying in {delay:?} (attempt {}/{})",
215 attempt + 1,
216 max_retries
217 );
218 dst::time::sleep(delay).await;
219 };
220
221 log::debug!("Connected");
222
223 let connection_mode = Arc::new(AtomicU8::new(ConnectionMode::Active.as_u8()));
224 let state_notify = Arc::new(tokio::sync::Notify::new());
225
226 let read_task = Arc::new(Self::spawn_read_task(
227 connection_mode.clone(),
228 reader,
229 message_handler.clone(),
230 suffix.clone(),
231 *idle_timeout_ms,
232 ));
233
234 let (writer_tx, writer_rx) = tokio::sync::mpsc::unbounded_channel::<WriterCommand>();
235
236 let write_task = Self::spawn_write_task(
237 connection_mode.clone(),
238 state_notify.clone(),
239 writer,
240 writer_rx,
241 suffix.clone(),
242 );
243
244 let heartbeat_task = heartbeat.as_ref().map(|heartbeat| {
246 Self::spawn_heartbeat_task(
247 connection_mode.clone(),
248 heartbeat.clone(),
249 writer_tx.clone(),
250 )
251 });
252
253 let reconnect_timeout = Duration::from_millis(reconnect_timeout_ms.unwrap_or(10_000));
254 let backoff = ExponentialBackoff::new(
255 Duration::from_millis(reconnect_delay_initial_ms.unwrap_or(2_000)),
256 Duration::from_millis(reconnect_delay_max_ms.unwrap_or(30_000)),
257 reconnect_backoff_factor.unwrap_or(1.5),
258 reconnect_jitter_ms.unwrap_or(100),
259 true, )?;
261
262 Ok(Self {
263 config,
264 connector,
265 read_task,
266 write_task,
267 writer_tx,
268 heartbeat_task,
269 connection_mode,
270 state_notify,
271 reconnect_timeout,
272 backoff,
273 handler: message_handler.clone(),
274 reconnect_max_attempts: *reconnect_max_attempts,
275 reconnect_attempt_count: 0,
276 })
277 }
278
279 fn parse_socket_url(url: &str, mode: Mode) -> Result<(String, String), Error> {
289 if url.contains("://") {
290 let parsed = url.parse::<http::Uri>().map_err(|e| {
292 Error::Io(std::io::Error::new(
293 std::io::ErrorKind::InvalidInput,
294 format!("Invalid URL: {e}"),
295 ))
296 })?;
297
298 let host = parsed.host().ok_or_else(|| {
299 Error::Io(std::io::Error::new(
300 std::io::ErrorKind::InvalidInput,
301 "URL missing host",
302 ))
303 })?;
304
305 let port = parsed
306 .port_u16()
307 .unwrap_or_else(|| match parsed.scheme_str() {
308 Some("wss" | "https") => 443,
309 Some("ws" | "http") => 80,
310 _ => match mode {
311 Mode::Tls => 443,
312 Mode::Plain => 80,
313 },
314 });
315
316 Ok((format!("{host}:{port}"), url.to_string()))
317 } else {
318 let scheme = match mode {
321 Mode::Tls => "wss",
322 Mode::Plain => "ws",
323 };
324 Ok((url.to_string(), format!("{scheme}://{url}")))
325 }
326 }
327
328 pub(crate) async fn tls_connect_with_server(
338 url: &str,
339 mode: Mode,
340 connector: Option<Connector>,
341 ) -> Result<(TcpReader, TcpWriter), Error> {
342 log::debug!("Connecting to {url}");
343
344 let (socket_addr, request_url) = Self::parse_socket_url(url, mode)?;
345 let tcp_result = TcpStream::connect(&socket_addr).await;
346
347 match tcp_result {
348 Ok(stream) => {
349 log::debug!("TCP connection established to {socket_addr}, proceeding with TLS");
350
351 if let Err(e) = stream.set_nodelay(true) {
352 log::warn!("Failed to enable TCP_NODELAY for socket client: {e:?}");
353 }
354 let request = request_url.into_client_request()?;
355 tcp_tls(&request, mode, stream, connector)
356 .await
357 .map(tokio::io::split)
358 }
359 Err(e) => {
360 log::warn!("TCP connection failed to {socket_addr}: {e:?}");
361 Err(Error::Io(e))
362 }
363 }
364 }
365
366 async fn reconnect(&mut self) -> Result<(), Error> {
377 log::debug!("Reconnecting");
378
379 if ConnectionMode::from_atomic(&self.connection_mode).is_disconnect() {
380 log::debug!("Reconnect aborted due to disconnect state");
381 return Ok(());
382 }
383
384 let SocketConfig {
385 url,
386 mode,
387 heartbeat: _,
388 suffix,
389 message_handler: _,
390 reconnect_timeout_ms: _,
391 reconnect_delay_initial_ms: _,
392 reconnect_backoff_factor: _,
393 reconnect_delay_max_ms: _,
394 reconnect_jitter_ms: _,
395 connection_max_retries: _,
396 reconnect_max_attempts: _,
397 idle_timeout_ms,
398 certs_dir: _,
399 } = &self.config;
400 let connector = self.connector.clone();
402
403 let (reader, new_writer) = dst::time::timeout(
405 self.reconnect_timeout,
406 Self::tls_connect_with_server(url, *mode, connector),
407 )
408 .await
409 .map_err(|_| {
410 Error::Io(std::io::Error::new(
411 std::io::ErrorKind::TimedOut,
412 format!(
413 "reconnection timed out after {}s",
414 self.reconnect_timeout.as_secs_f64()
415 ),
416 ))
417 })??;
418
419 if ConnectionMode::from_atomic(&self.connection_mode).is_disconnect() {
420 log::debug!("Reconnect aborted mid-flight (after connect)");
421 return Ok(());
422 }
423 log::debug!("Connected");
424
425 let (tx, rx) = tokio::sync::oneshot::channel();
429 if let Err(e) = self.writer_tx.send(WriterCommand::Update(new_writer, tx)) {
430 log::error!("{e}");
431 return Err(Error::Io(std::io::Error::new(
432 std::io::ErrorKind::BrokenPipe,
433 format!("Failed to send update command: {e}"),
434 )));
435 }
436
437 match rx.await {
439 Ok(true) => log::debug!("Writer confirmed buffer drain success"),
440 Ok(false) => {
441 log::warn!("Writer failed to drain buffer, aborting reconnect");
442 return Err(Error::Io(std::io::Error::other(
444 "Failed to drain reconnection buffer",
445 )));
446 }
447 Err(e) => {
448 log::error!("Writer dropped update channel: {e}");
449 return Err(Error::Io(std::io::Error::new(
450 std::io::ErrorKind::BrokenPipe,
451 "Writer task dropped response channel",
452 )));
453 }
454 }
455
456 dst::time::sleep(Duration::from_millis(GRACEFUL_SHUTDOWN_DELAY_MS)).await;
458
459 if ConnectionMode::from_atomic(&self.connection_mode).is_disconnect() {
460 log::debug!("Reconnect aborted mid-flight (after delay)");
461 return Ok(());
462 }
463
464 if !self.read_task.is_finished() {
465 self.read_task.abort();
466 log_task_aborted("read");
467 }
468
469 if self
472 .connection_mode
473 .compare_exchange(
474 ConnectionMode::Reconnect.as_u8(),
475 ConnectionMode::Active.as_u8(),
476 Ordering::SeqCst,
477 Ordering::SeqCst,
478 )
479 .is_err()
480 {
481 log::debug!("Reconnect aborted (state changed during reconnect)");
482 return Ok(());
483 }
484
485 self.read_task = Arc::new(Self::spawn_read_task(
487 self.connection_mode.clone(),
488 reader,
489 self.handler.clone(),
490 suffix.clone(),
491 *idle_timeout_ms,
492 ));
493
494 log::debug!("Reconnect succeeded");
495 Ok(())
496 }
497
498 #[inline]
504 #[must_use]
505 pub(crate) fn is_alive(&self) -> bool {
506 !self.read_task.is_finished() && !self.write_task.is_finished()
507 }
508
509 #[must_use]
510 fn spawn_read_task(
511 connection_state: Arc<AtomicU8>,
512 mut reader: TcpReader,
513 handler: Option<TcpMessageHandler>,
514 suffix: Vec<u8>,
515 idle_timeout_ms: Option<u64>,
516 ) -> tokio::task::JoinHandle<()> {
517 log_task_started("read");
518
519 let check_interval = Duration::from_millis(CONNECTION_STATE_CHECK_INTERVAL_MS);
521 let idle_timeout = idle_timeout_ms.map(Duration::from_millis);
522
523 tokio::task::spawn(async move {
524 let mut buf = Vec::new();
525 let mut last_data_time = dst::time::Instant::now();
526
527 loop {
528 if !ConnectionMode::from_atomic(&connection_state).is_active() {
529 break;
530 }
531
532 match dst::time::timeout(check_interval, reader.read_buf(&mut buf)).await {
533 Ok(Ok(0)) => {
535 log::debug!("Connection closed by server");
536 break;
537 }
538 Ok(Err(e)) => {
539 log::debug!("Connection ended: {e}");
540 break;
541 }
542 Ok(Ok(bytes)) => {
544 log::trace!("Received <binary> {bytes} bytes");
545 last_data_time = dst::time::Instant::now();
546
547 while let Some((i, _)) = &buf
548 .windows(suffix.len())
549 .enumerate()
550 .find(|(_, pair)| pair.eq(&suffix))
551 {
552 let mut data: Vec<u8> = buf.drain(0..i + suffix.len()).collect();
553 data.truncate(data.len() - suffix.len());
554
555 if let Some(ref handler) = handler {
556 handler(&data);
557 }
558 }
559
560 if buf.len() > MAX_READ_BUFFER_BYTES {
561 log::error!(
562 "Read buffer exceeded maximum size ({MAX_READ_BUFFER_BYTES} bytes), closing connection"
563 );
564 break;
565 }
566 }
567 Err(_) => {
568 if let Some(timeout) = idle_timeout {
569 let idle_duration = last_data_time.elapsed();
570 if idle_duration >= timeout {
571 log::warn!(
572 "Read idle timeout: no data received for {:.1}s",
573 idle_duration.as_secs_f64()
574 );
575 break;
576 }
577 }
578 }
579 }
580 }
581
582 log_task_stopped("read");
583 })
584 }
585
586 async fn drain_reconnect_buffer(
596 buffer: &mut VecDeque<Bytes>,
597 writer: &mut TcpWriter,
598 suffix: &[u8],
599 ) -> bool {
600 if buffer.is_empty() {
601 return false;
602 }
603
604 let initial_buffer_len = buffer.len();
605 log::info!("Sending {initial_buffer_len} buffered messages after reconnection");
606
607 let mut send_error_occurred = false;
608
609 while let Some(buffered_msg) = buffer.front() {
610 let mut combined_msg = Vec::with_capacity(buffered_msg.len() + suffix.len());
611 combined_msg.extend_from_slice(buffered_msg);
612 combined_msg.extend_from_slice(suffix);
613
614 if let Err(e) = writer.write_all(&combined_msg).await {
615 if is_connection_drop_io_error(&e) {
616 log::warn!(
617 "Failed to send buffered message with suffix after reconnection: {e}, {} messages remain in buffer",
618 buffer.len()
619 );
620 } else {
621 log::error!(
622 "Failed to send buffered message with suffix after reconnection: {e}, {} messages remain in buffer",
623 buffer.len()
624 );
625 }
626 send_error_occurred = true;
627 break;
628 }
629
630 buffer.pop_front();
631 }
632
633 if buffer.is_empty() {
634 log::info!("Successfully sent all {initial_buffer_len} buffered messages");
635 }
636
637 send_error_occurred
638 }
639
640 fn spawn_write_task(
641 connection_state: Arc<AtomicU8>,
642 state_notify: Arc<tokio::sync::Notify>,
643 writer: TcpWriter,
644 mut writer_rx: tokio::sync::mpsc::UnboundedReceiver<WriterCommand>,
645 suffix: Vec<u8>,
646 ) -> tokio::task::JoinHandle<()> {
647 log_task_started("write");
648
649 let check_interval = Duration::from_millis(CONNECTION_STATE_CHECK_INTERVAL_MS);
651
652 tokio::task::spawn(async move {
653 let mut active_writer = writer;
654 let mut reconnect_buffer: VecDeque<Bytes> = VecDeque::new();
655 let mut write_buf: Vec<u8> = Vec::new();
656
657 loop {
658 if matches!(
659 ConnectionMode::from_atomic(&connection_state),
660 ConnectionMode::Disconnect | ConnectionMode::Closed
661 ) {
662 break;
663 }
664
665 match dst::time::timeout(check_interval, writer_rx.recv()).await {
666 Ok(Some(msg)) => {
667 let mode = ConnectionMode::from_atomic(&connection_state);
669 if matches!(mode, ConnectionMode::Disconnect | ConnectionMode::Closed) {
670 break;
671 }
672
673 match msg {
674 WriterCommand::Update(new_writer, tx) => {
675 log::debug!("Received new writer");
676
677 dst::time::sleep(Duration::from_millis(100)).await;
679
680 _ = dst::time::timeout(
683 Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS),
684 active_writer.shutdown(),
685 )
686 .await;
687
688 active_writer = new_writer;
689 log::debug!("Updated writer");
690
691 let drain_result = dst::time::timeout(
694 Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS),
695 Self::drain_reconnect_buffer(
696 &mut reconnect_buffer,
697 &mut active_writer,
698 &suffix,
699 ),
700 )
701 .await;
702 let send_error = drain_result.unwrap_or_else(|_| {
703 log::warn!(
704 "Timed out draining reconnect buffer, {} messages remain",
705 reconnect_buffer.len()
706 );
707 true
708 });
709
710 if let Err(e) = tx.send(!send_error) {
711 log::error!(
712 "Failed to report drain status to controller: {e:?}"
713 );
714 }
715 }
716 _ if mode.is_reconnect() => {
717 if let WriterCommand::Send(data) = msg {
718 log::debug!(
719 "Buffering message while reconnecting ({} bytes)",
720 data.len()
721 );
722 reconnect_buffer.push_back(data);
723 }
724 }
725 WriterCommand::Send(msg) => {
726 write_buf.clear();
727 write_buf.extend_from_slice(&msg);
728 write_buf.extend_from_slice(&suffix);
729
730 if let Err(e) = active_writer.write_all(&write_buf).await {
731 if is_connection_drop_io_error(&e) {
732 log::warn!("Failed to send message: {e}");
733 } else {
734 log::error!("Failed to send message: {e}");
735 }
736 reconnect_buffer.push_back(msg);
737
738 if ConnectionMode::request_reconnect(&connection_state) {
740 log::warn!("Writer triggering reconnect");
741 state_notify.notify_one();
742 }
743 }
744 }
745 }
746 }
747 Ok(None) => {
748 log::debug!("Writer channel closed, terminating writer task");
750 break;
751 }
752 Err(_) => {
753 }
755 }
756 }
757
758 _ = dst::time::timeout(
761 Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS),
762 active_writer.shutdown(),
763 )
764 .await;
765
766 log_task_stopped("write");
767 })
768 }
769
770 fn spawn_heartbeat_task(
771 connection_state: Arc<AtomicU8>,
772 heartbeat: (u64, Vec<u8>),
773 writer_tx: tokio::sync::mpsc::UnboundedSender<WriterCommand>,
774 ) -> tokio::task::JoinHandle<()> {
775 log_task_started("heartbeat");
776 let (interval_secs, message) = heartbeat;
777
778 tokio::task::spawn(async move {
779 let interval = Duration::from_secs(interval_secs);
780
781 loop {
782 dst::time::sleep(interval).await;
783
784 match ConnectionMode::from_u8(connection_state.load(Ordering::SeqCst)) {
785 ConnectionMode::Active => {
786 let msg = WriterCommand::Send(message.clone().into());
787
788 match writer_tx.send(msg) {
789 Ok(()) => log::trace!("Sent heartbeat to writer task"),
790 Err(e) => {
791 log::error!("Failed to send heartbeat to writer task: {e}");
792 }
793 }
794 }
795 ConnectionMode::Reconnect => {}
796 ConnectionMode::Disconnect | ConnectionMode::Closed => break,
797 }
798 }
799
800 log_task_stopped("heartbeat");
801 })
802 }
803}
804
805impl Drop for SocketClientInner {
806 fn drop(&mut self) {
807 self.clean_drop();
809 }
810}
811
812impl CleanDrop for SocketClientInner {
814 fn clean_drop(&mut self) {
815 if !self.read_task.is_finished() {
816 self.read_task.abort();
817 log_task_aborted("read");
818 }
819
820 if !self.write_task.is_finished() {
821 self.write_task.abort();
822 log_task_aborted("write");
823 }
824
825 if let Some(ref handle) = self.heartbeat_task.take()
826 && !handle.is_finished()
827 {
828 handle.abort();
829 log_task_aborted("heartbeat");
830 }
831
832 #[cfg(feature = "python")]
833 {
834 self.config.message_handler = None;
836 }
837 }
838}
839
840#[cfg_attr(
841 feature = "python",
842 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network")
843)]
844#[cfg_attr(
845 feature = "python",
846 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.network")
847)]
848pub struct SocketClient {
849 pub(crate) controller_task: tokio::task::JoinHandle<()>,
850 pub(crate) connection_mode: Arc<AtomicU8>,
851 pub(crate) state_notify: Arc<tokio::sync::Notify>,
852 pub(crate) reconnect_timeout: Duration,
853 pub writer_tx: tokio::sync::mpsc::UnboundedSender<WriterCommand>,
854}
855
856impl Debug for SocketClient {
857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
858 f.debug_struct(stringify!(SocketClient)).finish()
859 }
860}
861
862impl SocketClient {
863 pub async fn connect(
869 config: SocketConfig,
870 post_connection: Option<Arc<dyn Fn() + Send + Sync>>,
871 post_reconnection: Option<Arc<dyn Fn() + Send + Sync>>,
872 post_disconnection: Option<Arc<dyn Fn() + Send + Sync>>,
873 ) -> anyhow::Result<Self> {
874 let inner = SocketClientInner::connect_url(config).await?;
875 let writer_tx = inner.writer_tx.clone();
876 let connection_mode = inner.connection_mode.clone();
877 let state_notify = inner.state_notify.clone();
878 let reconnect_timeout = inner.reconnect_timeout;
879
880 let controller_task = Self::spawn_controller_task(
881 inner,
882 connection_mode.clone(),
883 state_notify.clone(),
884 post_reconnection,
885 post_disconnection,
886 );
887
888 if let Some(handler) = post_connection {
889 handler();
890 log::debug!("Called `post_connection` handler");
891 }
892
893 Ok(Self {
894 controller_task,
895 connection_mode,
896 state_notify,
897 reconnect_timeout,
898 writer_tx,
899 })
900 }
901
902 #[must_use]
904 pub fn connection_mode(&self) -> ConnectionMode {
905 ConnectionMode::from_atomic(&self.connection_mode)
906 }
907
908 #[inline]
913 #[must_use]
914 pub fn is_active(&self) -> bool {
915 self.connection_mode().is_active()
916 }
917
918 #[inline]
923 #[must_use]
924 pub fn is_reconnecting(&self) -> bool {
925 self.connection_mode().is_reconnect()
926 }
927
928 #[inline]
932 #[must_use]
933 pub fn is_disconnecting(&self) -> bool {
934 self.connection_mode().is_disconnect()
935 }
936
937 #[inline]
943 #[must_use]
944 pub fn is_closed(&self) -> bool {
945 self.connection_mode().is_closed()
946 }
947
948 pub async fn close(&self) {
953 ConnectionMode::request_disconnect(&self.connection_mode);
955 self.state_notify.notify_waiters();
956
957 if dst::time::timeout(Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS), async {
958 while !self.is_closed() {
959 dst::time::sleep(Duration::from_millis(CONNECTION_STATE_CHECK_INTERVAL_MS)).await;
960 }
961
962 if !self.controller_task.is_finished() {
963 self.controller_task.abort();
964 log_task_aborted("controller");
965 }
966 })
967 .await
968 == Ok(())
969 {
970 log_task_stopped("controller");
971 } else {
972 log::warn!("Timeout waiting for controller task to finish");
973
974 if !self.controller_task.is_finished() {
975 self.controller_task.abort();
976 log_task_aborted("controller");
977 }
978 self.connection_mode
979 .store(ConnectionMode::Closed.as_u8(), Ordering::SeqCst);
980 }
981 }
982
983 #[inline]
987 fn check_not_terminal(&self) -> Result<(), SendError> {
988 match self.connection_mode() {
989 ConnectionMode::Disconnect | ConnectionMode::Closed => Err(SendError::Closed),
990 _ => Ok(()),
991 }
992 }
993
994 async fn wait_for_active(&self) -> Result<(), SendError> {
1000 const FALLBACK_INTERVAL_MS: u64 = 100;
1001
1002 let mode = self.connection_mode();
1003 if mode.is_active() {
1004 return Ok(());
1005 }
1006
1007 if matches!(mode, ConnectionMode::Disconnect | ConnectionMode::Closed) {
1008 return Err(SendError::Closed);
1009 }
1010
1011 log::debug!("Waiting for client to become ACTIVE before sending...");
1012
1013 let fallback_interval = Duration::from_millis(FALLBACK_INTERVAL_MS);
1014
1015 dst::time::timeout(self.reconnect_timeout, async {
1016 loop {
1017 let mut notified = pin!(self.state_notify.notified());
1019 notified.as_mut().enable();
1020
1021 let mode = self.connection_mode();
1022 if mode.is_active() {
1023 return Ok(());
1024 }
1025
1026 if matches!(mode, ConnectionMode::Disconnect | ConnectionMode::Closed) {
1027 return Err(());
1028 }
1029
1030 tokio::select! {
1031 biased;
1032 () = notified => {}
1033 () = dst::time::sleep(fallback_interval) => {}
1034 }
1035 }
1036 })
1037 .await
1038 .map_err(|_| SendError::Timeout)?
1039 .map_err(|()| SendError::Closed)
1040 }
1041
1042 pub async fn send_bytes(&self, data: Vec<u8>) -> Result<(), SendError> {
1052 self.check_not_terminal()?;
1053 self.wait_for_active().await?;
1054
1055 let msg = WriterCommand::Send(data.into());
1056 self.writer_tx
1057 .send(msg)
1058 .map_err(|e| SendError::BrokenPipe(e.to_string()))
1059 }
1060
1061 fn spawn_controller_task(
1062 mut inner: SocketClientInner,
1063 connection_mode: Arc<AtomicU8>,
1064 state_notify: Arc<tokio::sync::Notify>,
1065 post_reconnection: Option<Arc<dyn Fn() + Send + Sync>>,
1066 post_disconnection: Option<Arc<dyn Fn() + Send + Sync>>,
1067 ) -> tokio::task::JoinHandle<()> {
1068 const CONTROLLER_FALLBACK_INTERVAL_MS: u64 = 100;
1069
1070 tokio::task::spawn(async move {
1071 log_task_started("controller");
1072
1073 let fallback_interval = Duration::from_millis(CONTROLLER_FALLBACK_INTERVAL_MS);
1074
1075 loop {
1076 tokio::select! {
1077 biased;
1078 () = state_notify.notified() => {}
1079 () = dst::time::sleep(fallback_interval) => {}
1080 }
1081
1082 let mut mode = ConnectionMode::from_atomic(&connection_mode);
1083
1084 if mode.is_disconnect() {
1085 log::debug!("Disconnecting");
1086
1087 let timeout = Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
1088 if dst::time::timeout(timeout, async {
1089 dst::time::sleep(Duration::from_millis(GRACEFUL_SHUTDOWN_DELAY_MS)).await;
1091
1092 if !inner.read_task.is_finished() {
1093 inner.read_task.abort();
1094 log_task_aborted("read");
1095 }
1096
1097 if let Some(task) = &inner.heartbeat_task
1098 && !task.is_finished()
1099 {
1100 task.abort();
1101 log_task_aborted("heartbeat");
1102 }
1103 })
1104 .await
1105 .is_err()
1106 {
1107 log::warn!("Shutdown timed out after {}s", timeout.as_secs());
1108 }
1109
1110 log::debug!("Closed");
1111
1112 if let Some(ref handler) = post_disconnection {
1113 handler();
1114 log::debug!("Called `post_disconnection` handler");
1115 }
1116 break; }
1118
1119 if mode.is_closed() {
1120 log::debug!("Connection closed");
1121 break;
1122 }
1123
1124 if mode.is_active() && !inner.is_alive() {
1125 if connection_mode
1126 .compare_exchange(
1127 ConnectionMode::Active.as_u8(),
1128 ConnectionMode::Reconnect.as_u8(),
1129 Ordering::SeqCst,
1130 Ordering::SeqCst,
1131 )
1132 .is_ok()
1133 {
1134 log::debug!("Detected dead read task, transitioning to RECONNECT");
1135 }
1136 mode = ConnectionMode::from_atomic(&connection_mode);
1137 }
1138
1139 if mode.is_reconnect() {
1140 if let Some(max_attempts) = inner.reconnect_max_attempts
1142 && inner.reconnect_attempt_count >= max_attempts
1143 {
1144 log::error!(
1145 "Max reconnection attempts ({max_attempts}) exceeded, transitioning to CLOSED"
1146 );
1147 connection_mode.store(ConnectionMode::Closed.as_u8(), Ordering::SeqCst);
1148 state_notify.notify_waiters();
1149 break;
1150 }
1151
1152 inner.reconnect_attempt_count += 1;
1153
1154 let reconnect_result = tokio::select! {
1156 biased;
1157 result = inner.reconnect() => Some(result),
1158 () = async {
1159 loop {
1160 let mut notified = pin!(state_notify.notified());
1162 notified.as_mut().enable();
1163
1164 if ConnectionMode::from_atomic(&connection_mode).is_disconnect() {
1165 break;
1166 }
1167 notified.await;
1168 }
1169 } => None,
1170 };
1171
1172 match reconnect_result {
1173 None => {
1174 log::debug!("Reconnect interrupted by disconnect");
1175 }
1176 Some(Ok(())) => {
1177 log::debug!("Reconnected successfully");
1178 inner.backoff.reset();
1179 inner.reconnect_attempt_count = 0;
1180
1181 state_notify.notify_waiters();
1182
1183 if ConnectionMode::from_atomic(&connection_mode).is_active() {
1184 if let Some(ref handler) = post_reconnection {
1185 handler();
1186 log::debug!("Called `post_reconnection` handler");
1187 }
1188 } else {
1189 log::debug!(
1190 "Skipping post_reconnection handlers due to disconnect state"
1191 );
1192 }
1193 }
1194 Some(Err(e)) => {
1195 let duration = inner.backoff.next_duration();
1196 log::warn!(
1197 "Reconnect attempt {} failed: {e}",
1198 inner.reconnect_attempt_count
1199 );
1200
1201 if !duration.is_zero() {
1202 log::warn!("Backing off for {}s...", duration.as_secs_f64());
1203 tokio::select! {
1205 biased;
1206 () = dst::time::sleep(duration) => {}
1207 () = async {
1208 loop {
1209 let mut notified = pin!(state_notify.notified());
1211 notified.as_mut().enable();
1212
1213 if ConnectionMode::from_atomic(&connection_mode).is_disconnect() {
1214 break;
1215 }
1216 notified.await;
1217 }
1218 } => {
1219 log::debug!("Backoff interrupted by disconnect");
1220 }
1221 }
1222 }
1223 }
1224 }
1225 }
1226 }
1227 inner
1228 .connection_mode
1229 .store(ConnectionMode::Closed.as_u8(), Ordering::SeqCst);
1230
1231 log_task_stopped("controller");
1232 })
1233 }
1234}
1235
1236impl Drop for SocketClient {
1238 fn drop(&mut self) {
1239 if !self.controller_task.is_finished() {
1240 self.controller_task.abort();
1241 log_task_aborted("controller");
1242 }
1243 }
1244}
1245
1246#[cfg(test)]
1247#[cfg(feature = "python")]
1248#[cfg(not(feature = "turmoil"))]
1249#[cfg(not(all(feature = "simulation", madsim)))] #[cfg(target_os = "linux")] mod tests {
1252 use nautilus_common::testing::wait_until_async;
1253 use pyo3::Python;
1254 use tokio::{
1255 io::{AsyncReadExt, AsyncWriteExt},
1256 net::{TcpListener, TcpStream},
1257 sync::Mutex,
1258 task,
1259 time::{Duration, sleep},
1260 };
1261
1262 use super::*;
1263
1264 async fn bind_test_server() -> (u16, TcpListener) {
1265 let listener = TcpListener::bind("127.0.0.1:0")
1266 .await
1267 .expect("Failed to bind ephemeral port");
1268 let port = listener.local_addr().unwrap().port();
1269 (port, listener)
1270 }
1271
1272 async fn run_echo_server(mut socket: TcpStream) {
1273 let mut buf = Vec::new();
1274 loop {
1275 match socket.read_buf(&mut buf).await {
1276 Ok(0) => {
1277 break;
1278 }
1279 Ok(_n) => {
1280 while let Some(idx) = buf.array_windows().position(|w| w == b"\r\n") {
1281 let mut line = buf.drain(..idx + 2).collect::<Vec<u8>>();
1282 line.truncate(line.len() - 2);
1284
1285 if line == b"close" {
1286 let _ = socket.shutdown().await;
1287 return;
1288 }
1289
1290 let mut echo_data = line;
1291 echo_data.extend_from_slice(b"\r\n");
1292 if socket.write_all(&echo_data).await.is_err() {
1293 break;
1294 }
1295 }
1296 }
1297 Err(e) => {
1298 eprintln!("Server read error: {e}");
1299 break;
1300 }
1301 }
1302 }
1303 }
1304
1305 #[tokio::test]
1306 async fn test_basic_send_receive() {
1307 Python::initialize();
1308
1309 let (port, listener) = bind_test_server().await;
1310 let server_task = task::spawn(async move {
1311 let (socket, _) = listener.accept().await.unwrap();
1312 run_echo_server(socket).await;
1313 });
1314
1315 let config = SocketConfig {
1316 url: format!("127.0.0.1:{port}"),
1317 mode: Mode::Plain,
1318 suffix: b"\r\n".to_vec(),
1319 message_handler: None,
1320 heartbeat: None,
1321 reconnect_timeout_ms: None,
1322 reconnect_delay_initial_ms: None,
1323 reconnect_backoff_factor: None,
1324 reconnect_delay_max_ms: None,
1325 reconnect_jitter_ms: None,
1326 reconnect_max_attempts: None,
1327 connection_max_retries: None,
1328 idle_timeout_ms: None,
1329 certs_dir: None,
1330 };
1331
1332 let client = SocketClient::connect(config, None, None, None)
1333 .await
1334 .expect("Client connect failed unexpectedly");
1335
1336 client.send_bytes(b"Hello".into()).await.unwrap();
1337 client.send_bytes(b"World".into()).await.unwrap();
1338
1339 sleep(Duration::from_millis(100)).await;
1341
1342 client.send_bytes(b"close".into()).await.unwrap();
1343 server_task.await.unwrap();
1344 assert!(!client.is_closed());
1345 }
1346
1347 #[tokio::test]
1348 async fn test_reconnect_fail_exhausted() {
1349 Python::initialize();
1350
1351 let (port, listener) = bind_test_server().await;
1352 drop(listener); wait_until_async(
1356 || async {
1357 TcpStream::connect(format!("127.0.0.1:{port}"))
1358 .await
1359 .is_err()
1360 },
1361 Duration::from_secs(2),
1362 )
1363 .await;
1364
1365 let config = SocketConfig {
1366 url: format!("127.0.0.1:{port}"),
1367 mode: Mode::Plain,
1368 suffix: b"\r\n".to_vec(),
1369 message_handler: None,
1370 heartbeat: None,
1371 reconnect_timeout_ms: Some(100),
1372 reconnect_delay_initial_ms: Some(50),
1373 reconnect_backoff_factor: Some(1.0),
1374 reconnect_delay_max_ms: Some(50),
1375 reconnect_jitter_ms: Some(0),
1376 connection_max_retries: Some(1),
1377 reconnect_max_attempts: None,
1378 idle_timeout_ms: None,
1379 certs_dir: None,
1380 };
1381
1382 let client_res = SocketClient::connect(config, None, None, None).await;
1383 assert!(
1384 client_res.is_err(),
1385 "Should fail quickly with no server listening"
1386 );
1387 }
1388
1389 #[tokio::test]
1390 async fn test_user_disconnect() {
1391 Python::initialize();
1392
1393 let (port, listener) = bind_test_server().await;
1394 let server_task = task::spawn(async move {
1395 let (socket, _) = listener.accept().await.unwrap();
1396 let mut buf = [0u8; 1024];
1397 let _ = socket.try_read(&mut buf);
1398
1399 loop {
1400 sleep(Duration::from_secs(1)).await;
1401 }
1402 });
1403
1404 let config = SocketConfig {
1405 url: format!("127.0.0.1:{port}"),
1406 mode: Mode::Plain,
1407 suffix: b"\r\n".to_vec(),
1408 message_handler: None,
1409 heartbeat: None,
1410 reconnect_timeout_ms: None,
1411 reconnect_delay_initial_ms: None,
1412 reconnect_backoff_factor: None,
1413 reconnect_delay_max_ms: None,
1414 reconnect_jitter_ms: None,
1415 reconnect_max_attempts: None,
1416 connection_max_retries: None,
1417 idle_timeout_ms: None,
1418 certs_dir: None,
1419 };
1420
1421 let client = SocketClient::connect(config, None, None, None)
1422 .await
1423 .unwrap();
1424
1425 client.close().await;
1426 assert!(client.is_closed());
1427 server_task.abort();
1428 }
1429
1430 #[tokio::test]
1431 async fn test_close_after_closed_returns_fast_and_preserves_state() {
1432 Python::initialize();
1433
1434 let (port, listener) = bind_test_server().await;
1435
1436 let server_task = task::spawn(async move {
1437 let (socket, _) = listener.accept().await.unwrap();
1440 drop(socket);
1441 drop(listener);
1442 sleep(Duration::from_secs(5)).await;
1443 });
1444
1445 let config = SocketConfig {
1446 url: format!("127.0.0.1:{port}"),
1447 mode: Mode::Plain,
1448 suffix: b"\r\n".to_vec(),
1449 message_handler: None,
1450 heartbeat: None,
1451 reconnect_timeout_ms: Some(200),
1452 reconnect_delay_initial_ms: Some(50),
1453 reconnect_backoff_factor: Some(1.0),
1454 reconnect_delay_max_ms: Some(50),
1455 reconnect_jitter_ms: Some(0),
1456 connection_max_retries: None,
1457 reconnect_max_attempts: Some(1),
1458 idle_timeout_ms: None,
1459 certs_dir: None,
1460 };
1461
1462 let client = SocketClient::connect(config, None, None, None)
1463 .await
1464 .unwrap();
1465
1466 wait_until_async(|| async { client.is_closed() }, Duration::from_secs(5)).await;
1467
1468 let start = std::time::Instant::now();
1472 client.close().await;
1473 let elapsed = start.elapsed();
1474
1475 assert!(client.is_closed(), "Client should remain CLOSED");
1476 assert!(
1477 !client.is_disconnecting(),
1478 "Closed client should not report DISCONNECT after close()"
1479 );
1480 assert!(
1481 elapsed < Duration::from_secs(2),
1482 "close() on a closed client should return fast, took {elapsed:?}"
1483 );
1484
1485 server_task.abort();
1486 }
1487
1488 #[tokio::test]
1489 async fn test_heartbeat() {
1490 Python::initialize();
1491
1492 let (port, listener) = bind_test_server().await;
1493 let received = Arc::new(Mutex::new(Vec::new()));
1494 let received2 = received.clone();
1495
1496 let server_task = task::spawn(async move {
1497 let (socket, _) = listener.accept().await.unwrap();
1498
1499 let mut buf = Vec::new();
1500 loop {
1501 match socket.try_read_buf(&mut buf) {
1502 Ok(0) => break,
1503 Ok(_) => {
1504 while let Some(idx) = buf.array_windows().position(|w| w == b"\r\n") {
1505 let mut line = buf.drain(..idx + 2).collect::<Vec<u8>>();
1506 line.truncate(line.len() - 2);
1507 received2.lock().await.push(line);
1508 }
1509 }
1510 Err(_) => {
1511 tokio::time::sleep(Duration::from_millis(10)).await;
1512 }
1513 }
1514 }
1515 });
1516
1517 let heartbeat = Some((1, b"ping".to_vec()));
1519
1520 let config = SocketConfig {
1521 url: format!("127.0.0.1:{port}"),
1522 mode: Mode::Plain,
1523 suffix: b"\r\n".to_vec(),
1524 message_handler: None,
1525 heartbeat,
1526 reconnect_timeout_ms: None,
1527 reconnect_delay_initial_ms: None,
1528 reconnect_backoff_factor: None,
1529 reconnect_delay_max_ms: None,
1530 reconnect_jitter_ms: None,
1531 reconnect_max_attempts: None,
1532 connection_max_retries: None,
1533 idle_timeout_ms: None,
1534 certs_dir: None,
1535 };
1536
1537 let client = SocketClient::connect(config, None, None, None)
1538 .await
1539 .unwrap();
1540
1541 sleep(Duration::from_secs(3)).await;
1543
1544 {
1545 let lock = received.lock().await;
1546 let pings = lock
1547 .iter()
1548 .filter(|line| line == &&b"ping".to_vec())
1549 .count();
1550 assert!(
1551 pings >= 2,
1552 "Expected at least 2 heartbeat pings; got {pings}"
1553 );
1554 }
1555
1556 client.close().await;
1557 server_task.abort();
1558 }
1559
1560 #[tokio::test]
1561 async fn test_reconnect_success() {
1562 Python::initialize();
1563
1564 let (port, listener) = bind_test_server().await;
1565
1566 let server_task = task::spawn(async move {
1570 let (mut socket, _) = listener.accept().await.expect("First accept failed");
1572
1573 sleep(Duration::from_millis(500)).await;
1575 let _ = socket.shutdown().await;
1576
1577 sleep(Duration::from_millis(500)).await;
1579
1580 let (socket, _) = listener.accept().await.expect("Second accept failed");
1582 run_echo_server(socket).await;
1583 });
1584
1585 let config = SocketConfig {
1586 url: format!("127.0.0.1:{port}"),
1587 mode: Mode::Plain,
1588 suffix: b"\r\n".to_vec(),
1589 message_handler: None,
1590 heartbeat: None,
1591 reconnect_timeout_ms: Some(5_000),
1592 reconnect_delay_initial_ms: Some(500),
1593 reconnect_delay_max_ms: Some(5_000),
1594 reconnect_backoff_factor: Some(2.0),
1595 reconnect_jitter_ms: Some(50),
1596 reconnect_max_attempts: None,
1597 connection_max_retries: None,
1598 idle_timeout_ms: None,
1599 certs_dir: None,
1600 };
1601
1602 let client = SocketClient::connect(config, None, None, None)
1603 .await
1604 .expect("Client connect failed unexpectedly");
1605
1606 assert!(client.is_active(), "Client should start as active");
1608
1609 wait_until_async(|| async { client.is_active() }, Duration::from_secs(10)).await;
1612
1613 client
1614 .send_bytes(b"TestReconnect".into())
1615 .await
1616 .expect("Send failed");
1617
1618 client.close().await;
1619 server_task.abort();
1620 }
1621}
1622
1623#[cfg(test)]
1624#[cfg(not(feature = "turmoil"))]
1625#[cfg(not(all(feature = "simulation", madsim)))] mod rust_tests {
1627 use nautilus_common::testing::wait_until_async;
1628 use rstest::rstest;
1629 use tokio::{
1630 io::{AsyncReadExt, AsyncWriteExt},
1631 net::TcpListener,
1632 task,
1633 time::{Duration, sleep},
1634 };
1635
1636 use super::*;
1637
1638 #[rstest]
1639 #[tokio::test]
1640 async fn test_reconnect_then_close() {
1641 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1643 let port = listener.local_addr().unwrap().port();
1644
1645 let server = task::spawn(async move {
1647 if let Ok((mut sock, _)) = listener.accept().await {
1648 drop(sock.shutdown());
1649 }
1650 sleep(Duration::from_secs(1)).await;
1652 });
1653
1654 let config = SocketConfig {
1656 url: format!("127.0.0.1:{port}"),
1657 mode: Mode::Plain,
1658 suffix: b"\r\n".to_vec(),
1659 message_handler: None,
1660 heartbeat: None,
1661 reconnect_timeout_ms: Some(1_000),
1662 reconnect_delay_initial_ms: Some(50),
1663 reconnect_delay_max_ms: Some(100),
1664 reconnect_backoff_factor: Some(1.0),
1665 reconnect_jitter_ms: Some(0),
1666 connection_max_retries: Some(1),
1667 reconnect_max_attempts: None,
1668 idle_timeout_ms: None,
1669 certs_dir: None,
1670 };
1671
1672 let client = SocketClient::connect(config.clone(), None, None, None)
1674 .await
1675 .unwrap();
1676
1677 wait_until_async(
1679 || async { client.is_reconnecting() },
1680 Duration::from_secs(2),
1681 )
1682 .await;
1683
1684 client.close().await;
1686 assert!(client.is_closed());
1687 server.abort();
1688 }
1689
1690 #[rstest]
1691 #[tokio::test]
1692 async fn test_reconnect_state_flips_when_reader_stops() {
1693 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1695 let port = listener.local_addr().unwrap().port();
1696
1697 let server = task::spawn(async move {
1698 if let Ok((sock, _)) = listener.accept().await {
1699 drop(sock);
1700 }
1701 sleep(Duration::from_millis(50)).await;
1703 });
1704
1705 let config = SocketConfig {
1706 url: format!("127.0.0.1:{port}"),
1707 mode: Mode::Plain,
1708 suffix: b"\r\n".to_vec(),
1709 message_handler: None,
1710 heartbeat: None,
1711 reconnect_timeout_ms: Some(1_000),
1712 reconnect_delay_initial_ms: Some(50),
1713 reconnect_delay_max_ms: Some(100),
1714 reconnect_backoff_factor: Some(1.0),
1715 reconnect_jitter_ms: Some(0),
1716 connection_max_retries: Some(1),
1717 reconnect_max_attempts: None,
1718 idle_timeout_ms: None,
1719 certs_dir: None,
1720 };
1721
1722 let client = SocketClient::connect(config, None, None, None)
1723 .await
1724 .unwrap();
1725
1726 wait_until_async(
1727 || async { client.is_reconnecting() },
1728 Duration::from_secs(2),
1729 )
1730 .await;
1731
1732 client.close().await;
1733 server.abort();
1734 }
1735
1736 #[rstest]
1737 fn test_parse_socket_url_raw_address() {
1738 let (socket_addr, request_url) =
1740 SocketClientInner::parse_socket_url("example.com:6130", Mode::Tls).unwrap();
1741 assert_eq!(socket_addr, "example.com:6130");
1742 assert_eq!(request_url, "wss://example.com:6130");
1743
1744 let (socket_addr, request_url) =
1746 SocketClientInner::parse_socket_url("localhost:8080", Mode::Plain).unwrap();
1747 assert_eq!(socket_addr, "localhost:8080");
1748 assert_eq!(request_url, "ws://localhost:8080");
1749 }
1750
1751 #[rstest]
1752 fn test_parse_socket_url_with_scheme() {
1753 let (socket_addr, request_url) =
1755 SocketClientInner::parse_socket_url("wss://example.com:443/path", Mode::Tls).unwrap();
1756 assert_eq!(socket_addr, "example.com:443");
1757 assert_eq!(request_url, "wss://example.com:443/path");
1758
1759 let (socket_addr, request_url) =
1761 SocketClientInner::parse_socket_url("ws://localhost:8080", Mode::Plain).unwrap();
1762 assert_eq!(socket_addr, "localhost:8080");
1763 assert_eq!(request_url, "ws://localhost:8080");
1764 }
1765
1766 #[rstest]
1767 fn test_parse_socket_url_default_ports() {
1768 let (socket_addr, _) =
1770 SocketClientInner::parse_socket_url("wss://example.com", Mode::Tls).unwrap();
1771 assert_eq!(socket_addr, "example.com:443");
1772
1773 let (socket_addr, _) =
1775 SocketClientInner::parse_socket_url("ws://example.com", Mode::Plain).unwrap();
1776 assert_eq!(socket_addr, "example.com:80");
1777
1778 let (socket_addr, _) =
1780 SocketClientInner::parse_socket_url("https://example.com", Mode::Tls).unwrap();
1781 assert_eq!(socket_addr, "example.com:443");
1782
1783 let (socket_addr, _) =
1785 SocketClientInner::parse_socket_url("http://example.com", Mode::Plain).unwrap();
1786 assert_eq!(socket_addr, "example.com:80");
1787 }
1788
1789 #[rstest]
1790 fn test_parse_socket_url_unknown_scheme_uses_mode() {
1791 let (socket_addr, _) =
1793 SocketClientInner::parse_socket_url("custom://example.com", Mode::Tls).unwrap();
1794 assert_eq!(socket_addr, "example.com:443");
1795
1796 let (socket_addr, _) =
1797 SocketClientInner::parse_socket_url("custom://example.com", Mode::Plain).unwrap();
1798 assert_eq!(socket_addr, "example.com:80");
1799 }
1800
1801 #[rstest]
1802 fn test_parse_socket_url_ipv6() {
1803 let (socket_addr, request_url) =
1805 SocketClientInner::parse_socket_url("[::1]:8080", Mode::Plain).unwrap();
1806 assert_eq!(socket_addr, "[::1]:8080");
1807 assert_eq!(request_url, "ws://[::1]:8080");
1808
1809 let (socket_addr, _) =
1811 SocketClientInner::parse_socket_url("ws://[::1]:8080", Mode::Plain).unwrap();
1812 assert_eq!(socket_addr, "[::1]:8080");
1813 }
1814
1815 #[rstest]
1816 #[tokio::test]
1817 async fn test_url_parsing_raw_socket_address() {
1818 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1820 let port = listener.local_addr().unwrap().port();
1821
1822 let server = task::spawn(async move {
1823 if let Ok((sock, _)) = listener.accept().await {
1824 drop(sock);
1825 }
1826 sleep(Duration::from_millis(50)).await;
1827 });
1828
1829 let config = SocketConfig {
1830 url: format!("127.0.0.1:{port}"), mode: Mode::Plain,
1832 suffix: b"\r\n".to_vec(),
1833 message_handler: None,
1834 heartbeat: None,
1835 reconnect_timeout_ms: Some(1_000),
1836 reconnect_delay_initial_ms: Some(50),
1837 reconnect_delay_max_ms: Some(100),
1838 reconnect_backoff_factor: Some(1.0),
1839 reconnect_jitter_ms: Some(0),
1840 connection_max_retries: Some(1),
1841 reconnect_max_attempts: None,
1842 idle_timeout_ms: None,
1843 certs_dir: None,
1844 };
1845
1846 let client = SocketClient::connect(config, None, None, None).await;
1848 assert!(
1849 client.is_ok(),
1850 "Client should connect with raw socket address format"
1851 );
1852
1853 if let Ok(client) = client {
1854 client.close().await;
1855 }
1856 server.abort();
1857 }
1858
1859 #[rstest]
1860 #[tokio::test]
1861 async fn test_url_parsing_with_scheme() {
1862 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1864 let port = listener.local_addr().unwrap().port();
1865
1866 let server = task::spawn(async move {
1867 if let Ok((sock, _)) = listener.accept().await {
1868 drop(sock);
1869 }
1870 sleep(Duration::from_millis(50)).await;
1871 });
1872
1873 let config = SocketConfig {
1874 url: format!("ws://127.0.0.1:{port}"), mode: Mode::Plain,
1876 suffix: b"\r\n".to_vec(),
1877 message_handler: None,
1878 heartbeat: None,
1879 reconnect_timeout_ms: Some(1_000),
1880 reconnect_delay_initial_ms: Some(50),
1881 reconnect_delay_max_ms: Some(100),
1882 reconnect_backoff_factor: Some(1.0),
1883 reconnect_jitter_ms: Some(0),
1884 connection_max_retries: Some(1),
1885 reconnect_max_attempts: None,
1886 idle_timeout_ms: None,
1887 certs_dir: None,
1888 };
1889
1890 let client = SocketClient::connect(config, None, None, None).await;
1892 assert!(
1893 client.is_ok(),
1894 "Client should connect with URL scheme format"
1895 );
1896
1897 if let Ok(client) = client {
1898 client.close().await;
1899 }
1900 server.abort();
1901 }
1902
1903 #[rstest]
1904 fn test_parse_socket_url_ipv6_with_zone() {
1905 let (socket_addr, request_url) =
1907 SocketClientInner::parse_socket_url("[fe80::1%eth0]:8080", Mode::Plain).unwrap();
1908 assert_eq!(socket_addr, "[fe80::1%eth0]:8080");
1909 assert_eq!(request_url, "ws://[fe80::1%eth0]:8080");
1910
1911 let (socket_addr, request_url) =
1913 SocketClientInner::parse_socket_url("ws://[fe80::1%lo]:9090", Mode::Plain).unwrap();
1914 assert_eq!(socket_addr, "[fe80::1%lo]:9090");
1915 assert_eq!(request_url, "ws://[fe80::1%lo]:9090");
1916 }
1917
1918 #[rstest]
1919 #[tokio::test]
1920 async fn test_ipv6_loopback_connection() {
1921 if TcpListener::bind("[::1]:0").await.is_err() {
1924 eprintln!("IPv6 not available, skipping test");
1925 return;
1926 }
1927
1928 let listener = TcpListener::bind("[::1]:0").await.unwrap();
1929 let port = listener.local_addr().unwrap().port();
1930
1931 let server = task::spawn(async move {
1932 if let Ok((mut sock, _)) = listener.accept().await {
1933 let mut buf = vec![0u8; 1024];
1934 if let Ok(n) = sock.read(&mut buf).await {
1935 let _ = sock.write_all(&buf[..n]).await;
1937 }
1938 }
1939 sleep(Duration::from_millis(50)).await;
1940 });
1941
1942 let config = SocketConfig {
1943 url: format!("[::1]:{port}"), mode: Mode::Plain,
1945 suffix: b"\r\n".to_vec(),
1946 message_handler: None,
1947 heartbeat: None,
1948 reconnect_timeout_ms: Some(1_000),
1949 reconnect_delay_initial_ms: Some(50),
1950 reconnect_delay_max_ms: Some(100),
1951 reconnect_backoff_factor: Some(1.0),
1952 reconnect_jitter_ms: Some(0),
1953 connection_max_retries: Some(1),
1954 reconnect_max_attempts: None,
1955 idle_timeout_ms: None,
1956 certs_dir: None,
1957 };
1958
1959 let client = SocketClient::connect(config, None, None, None).await;
1960 assert!(
1961 client.is_ok(),
1962 "Client should connect to IPv6 loopback address"
1963 );
1964
1965 if let Ok(client) = client {
1966 client.close().await;
1967 }
1968 server.abort();
1969 }
1970
1971 #[rstest]
1972 #[tokio::test]
1973 async fn test_send_waits_during_reconnection() {
1974 use nautilus_common::testing::wait_until_async;
1976
1977 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1978 let port = listener.local_addr().unwrap().port();
1979
1980 let server = task::spawn(async move {
1981 if let Ok((sock, _)) = listener.accept().await {
1983 drop(sock);
1984 }
1985
1986 sleep(Duration::from_millis(500)).await;
1988
1989 if let Ok((mut sock, _)) = listener.accept().await {
1991 let mut buf = vec![0u8; 1024];
1993 while let Ok(n) = sock.read(&mut buf).await {
1994 if n == 0 {
1995 break;
1996 }
1997
1998 if sock.write_all(&buf[..n]).await.is_err() {
1999 break;
2000 }
2001 }
2002 }
2003 });
2004
2005 let config = SocketConfig {
2006 url: format!("127.0.0.1:{port}"),
2007 mode: Mode::Plain,
2008 suffix: b"\r\n".to_vec(),
2009 message_handler: None,
2010 heartbeat: None,
2011 reconnect_timeout_ms: Some(5_000), reconnect_delay_initial_ms: Some(100),
2013 reconnect_delay_max_ms: Some(200),
2014 reconnect_backoff_factor: Some(1.0),
2015 reconnect_jitter_ms: Some(0),
2016 connection_max_retries: Some(1),
2017 reconnect_max_attempts: None,
2018 idle_timeout_ms: None,
2019 certs_dir: None,
2020 };
2021
2022 let client = SocketClient::connect(config, None, None, None)
2023 .await
2024 .unwrap();
2025
2026 wait_until_async(
2028 || async { client.is_reconnecting() },
2029 Duration::from_secs(2),
2030 )
2031 .await;
2032
2033 let send_result = tokio::time::timeout(
2035 Duration::from_secs(3),
2036 client.send_bytes(b"test_message".to_vec()),
2037 )
2038 .await;
2039
2040 assert!(
2041 send_result.is_ok() && send_result.unwrap().is_ok(),
2042 "Send should succeed after waiting for reconnection"
2043 );
2044
2045 client.close().await;
2046 server.abort();
2047 }
2048
2049 #[rstest]
2050 #[tokio::test]
2051 async fn test_send_bytes_timeout_uses_configured_reconnect_timeout() {
2052 use nautilus_common::testing::wait_until_async;
2055
2056 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2057 let port = listener.local_addr().unwrap().port();
2058
2059 let server = task::spawn(async move {
2060 if let Ok((sock, _)) = listener.accept().await {
2062 drop(sock);
2063 }
2064 drop(listener);
2066 sleep(Duration::from_mins(1)).await;
2067 });
2068
2069 let config = SocketConfig {
2070 url: format!("127.0.0.1:{port}"),
2071 mode: Mode::Plain,
2072 suffix: b"\r\n".to_vec(),
2073 message_handler: None,
2074 heartbeat: None,
2075 reconnect_timeout_ms: Some(1_000), reconnect_delay_initial_ms: Some(200), reconnect_delay_max_ms: Some(200),
2078 reconnect_backoff_factor: Some(1.0),
2079 reconnect_jitter_ms: Some(0),
2080 connection_max_retries: Some(1),
2081 reconnect_max_attempts: None,
2082 idle_timeout_ms: None,
2083 certs_dir: None,
2084 };
2085
2086 let client = SocketClient::connect(config, None, None, None)
2087 .await
2088 .unwrap();
2089
2090 wait_until_async(
2092 || async { client.is_reconnecting() },
2093 Duration::from_secs(3),
2094 )
2095 .await;
2096
2097 let start = std::time::Instant::now();
2100 let send_result = client.send_bytes(b"test".to_vec()).await;
2101 let elapsed = start.elapsed();
2102
2103 assert!(
2104 send_result.is_err(),
2105 "Send should fail when client stuck in RECONNECT, was: {send_result:?}"
2106 );
2107 assert!(
2108 matches!(send_result, Err(crate::error::SendError::Timeout)),
2109 "Send should return Timeout error, was: {send_result:?}"
2110 );
2111 assert!(
2114 elapsed >= Duration::from_millis(900),
2115 "Send should timeout after at least 1s (configured timeout), took {elapsed:?}"
2116 );
2117
2118 client.close().await;
2119 server.abort();
2120 }
2121
2122 #[rstest]
2123 #[tokio::test]
2124 async fn test_idle_timeout_triggers_reconnect() {
2125 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2126 let port = listener.local_addr().unwrap().port();
2127
2128 let server = task::spawn(async move {
2130 let (_sock1, _) = listener.accept().await.unwrap();
2131 sleep(Duration::from_secs(5)).await;
2133 });
2134
2135 let config = SocketConfig {
2136 url: format!("127.0.0.1:{port}"),
2137 mode: Mode::Plain,
2138 suffix: b"\r\n".to_vec(),
2139 message_handler: None,
2140 heartbeat: None,
2141 reconnect_timeout_ms: Some(2_000),
2142 reconnect_delay_initial_ms: Some(50),
2143 reconnect_delay_max_ms: Some(100),
2144 reconnect_backoff_factor: Some(1.0),
2145 reconnect_jitter_ms: Some(0),
2146 connection_max_retries: Some(1),
2147 reconnect_max_attempts: Some(1),
2148 idle_timeout_ms: Some(500),
2149 certs_dir: None,
2150 };
2151
2152 let client = SocketClient::connect(config, None, None, None)
2153 .await
2154 .unwrap();
2155
2156 assert!(client.is_active());
2157
2158 wait_until_async(
2160 || async { client.is_reconnecting() || client.is_closed() },
2161 Duration::from_secs(3),
2162 )
2163 .await;
2164
2165 assert!(
2166 !client.is_active(),
2167 "Client should not be active after idle timeout"
2168 );
2169
2170 client.close().await;
2171 server.abort();
2172 }
2173
2174 #[rstest]
2175 #[tokio::test]
2176 async fn test_idle_timeout_resets_on_data() {
2177 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2178 let port = listener.local_addr().unwrap().port();
2179
2180 let server = task::spawn(async move {
2182 let (mut sock, _) = listener.accept().await.unwrap();
2183 for _ in 0..10 {
2184 sleep(Duration::from_millis(200)).await;
2185
2186 if sock.write_all(b"ping\r\n").await.is_err() {
2187 break;
2188 }
2189 }
2190 });
2191
2192 let config = SocketConfig {
2193 url: format!("127.0.0.1:{port}"),
2194 mode: Mode::Plain,
2195 suffix: b"\r\n".to_vec(),
2196 message_handler: None,
2197 heartbeat: None,
2198 reconnect_timeout_ms: Some(2_000),
2199 reconnect_delay_initial_ms: Some(50),
2200 reconnect_delay_max_ms: Some(100),
2201 reconnect_backoff_factor: Some(1.0),
2202 reconnect_jitter_ms: Some(0),
2203 connection_max_retries: Some(1),
2204 reconnect_max_attempts: Some(1),
2205 idle_timeout_ms: Some(1_000),
2206 certs_dir: None,
2207 };
2208
2209 let client = SocketClient::connect(config, None, None, None)
2210 .await
2211 .unwrap();
2212
2213 assert!(client.is_active());
2214
2215 sleep(Duration::from_millis(1_500)).await;
2217
2218 assert!(
2219 client.is_active(),
2220 "Client should remain active when data is flowing"
2221 );
2222
2223 client.close().await;
2224 server.abort();
2225 }
2226
2227 #[rstest]
2228 #[tokio::test]
2229 async fn test_close_during_backoff_exits_promptly() {
2230 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2234 let port = listener.local_addr().unwrap().port();
2235
2236 let server = task::spawn(async move {
2237 if let Ok((mut sock, _)) = listener.accept().await {
2239 drop(sock.shutdown());
2240 }
2241 sleep(Duration::from_mins(1)).await;
2243 });
2244
2245 let config = SocketConfig {
2246 url: format!("127.0.0.1:{port}"),
2247 mode: Mode::Plain,
2248 suffix: b"\r\n".to_vec(),
2249 message_handler: None,
2250 heartbeat: None,
2251 reconnect_timeout_ms: Some(1_000),
2252 reconnect_delay_initial_ms: Some(10_000), reconnect_delay_max_ms: Some(10_000),
2254 reconnect_backoff_factor: Some(1.0),
2255 reconnect_jitter_ms: Some(0),
2256 connection_max_retries: None,
2257 reconnect_max_attempts: None,
2258 idle_timeout_ms: None,
2259 certs_dir: None,
2260 };
2261
2262 let client = SocketClient::connect(config, None, None, None)
2263 .await
2264 .unwrap();
2265
2266 wait_until_async(
2268 || async { client.is_reconnecting() },
2269 Duration::from_secs(3),
2270 )
2271 .await;
2272
2273 sleep(Duration::from_millis(1_500)).await;
2275
2276 let start = std::time::Instant::now();
2278 client.close().await;
2279 let elapsed = start.elapsed();
2280
2281 assert!(client.is_closed(), "Client should be closed");
2282 assert!(
2284 elapsed < Duration::from_secs(2),
2285 "Close should interrupt backoff sleep, took {elapsed:?}"
2286 );
2287
2288 server.abort();
2289 }
2290
2291 #[rstest]
2292 #[tokio::test]
2293 async fn test_zero_idle_timeout_rejected() {
2294 let config = SocketConfig {
2295 url: "127.0.0.1:9999".to_string(),
2296 mode: Mode::Plain,
2297 suffix: b"\r\n".to_vec(),
2298 message_handler: None,
2299 heartbeat: None,
2300 reconnect_timeout_ms: None,
2301 reconnect_delay_initial_ms: None,
2302 reconnect_delay_max_ms: None,
2303 reconnect_backoff_factor: None,
2304 reconnect_jitter_ms: None,
2305 reconnect_max_attempts: None,
2306 connection_max_retries: Some(1),
2307 idle_timeout_ms: Some(0),
2308 certs_dir: None,
2309 };
2310
2311 let result = SocketClient::connect(config, None, None, None).await;
2312
2313 assert!(result.is_err(), "Zero idle timeout should be rejected");
2314 let err_msg = result.unwrap_err().to_string();
2315 assert!(
2316 err_msg.contains("Idle timeout cannot be zero"),
2317 "Error should mention zero idle timeout, was: {err_msg}"
2318 );
2319 }
2320
2321 #[rstest]
2322 #[tokio::test]
2323 async fn test_empty_suffix_rejected() {
2324 let config = SocketConfig {
2325 url: "127.0.0.1:9999".to_string(),
2326 mode: Mode::Plain,
2327 suffix: vec![],
2328 message_handler: None,
2329 heartbeat: None,
2330 reconnect_timeout_ms: None,
2331 reconnect_delay_initial_ms: None,
2332 reconnect_delay_max_ms: None,
2333 reconnect_backoff_factor: None,
2334 reconnect_jitter_ms: None,
2335 reconnect_max_attempts: None,
2336 connection_max_retries: Some(1),
2337 idle_timeout_ms: None,
2338 certs_dir: None,
2339 };
2340
2341 let result = SocketClient::connect(config, None, None, None).await;
2342
2343 assert!(
2344 result.is_err(),
2345 "Empty suffix should cause connection to fail"
2346 );
2347 let err_msg = result.unwrap_err().to_string();
2348 assert!(
2349 err_msg.contains("suffix cannot be empty"),
2350 "Error should mention empty suffix, was: {err_msg}"
2351 );
2352 }
2353}