1use futures_util::{SinkExt, StreamExt};
2use std::time::Duration;
3use tokio::{
4 sync::mpsc,
5 time::{Instant, sleep, sleep_until, timeout},
6};
7use tokio_tungstenite::{connect_async, tungstenite::Message};
8use tracing::{debug, error, info, warn};
9
10use crate::{serde::serialize_json, ws::IncomingMessage};
11
12use super::{
13 Command, Config, DisconnectReason, Event, Handle,
14 state::{FrameResult, HeartbeatState, Sink, State},
15};
16
17pub struct Stream {
36 config: Config,
37 cmd_rx: mpsc::Receiver<Command>,
38 evt_tx: mpsc::Sender<Event>,
39 dropped: u64,
41}
42
43impl Stream {
44 #[allow(clippy::new_ret_no_self)]
45 pub fn new(config: Config) -> (Handle, mpsc::Receiver<Event>) {
46 let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(config.command_queue_size);
47 let (evt_tx, evt_rx) = mpsc::channel::<Event>(config.event_queue_size);
48
49 let stream = Self {
50 config,
51 cmd_rx,
52 evt_tx,
53 dropped: 0,
54 };
55
56 tokio::spawn(stream.run());
57
58 (Handle::new(cmd_tx), evt_rx)
59 }
60
61 async fn run(mut self) {
62 info!("stream started");
63 let mut state = State::Idle;
64
65 loop {
66 state = match state {
67 State::Idle => self.step_idle().await,
68 State::Connecting { attempt } => self.step_connecting(attempt).await,
69 State::Connected {
70 frame_rx,
71 read_task,
72 sink,
73 } => self.step_connected(frame_rx, read_task, sink).await,
74 State::Reconnecting { attempt, delay_ms } => {
75 self.step_reconnecting(attempt, delay_ms).await
76 }
77 State::Closing {
78 frame_rx,
79 read_task,
80 sink,
81 } => self.step_closing(frame_rx, read_task, sink).await,
82 State::Done => break,
83 };
84 }
85
86 info!("stream shut down");
87 }
88
89 fn emit(&mut self, event: Event) {
92 if self.dropped > 0 {
93 match self.evt_tx.try_send(Event::Lagged {
94 dropped: self.dropped,
95 }) {
96 Ok(()) => self.dropped = 0,
97 Err(mpsc::error::TrySendError::Full(_)) => {
98 self.dropped += 1;
99 return;
100 }
101 Err(mpsc::error::TrySendError::Closed(_)) => return,
102 }
103 }
104
105 match self.evt_tx.try_send(event) {
106 Ok(()) => {}
107 Err(mpsc::error::TrySendError::Full(dropped)) => {
108 if self.dropped == 0 {
109 warn!("event queue full, dropping events");
110 }
111 debug!(event = ?dropped, "event dropped");
112 self.dropped += 1;
113 }
114 Err(mpsc::error::TrySendError::Closed(_)) => debug!("event receiver dropped"),
115 }
116 }
117
118 async fn emit_lifecycle(&mut self, event: Event) {
121 if self.dropped > 0 {
122 let lagged = Event::Lagged {
123 dropped: self.dropped,
124 };
125 if self.evt_tx.send(lagged).await.is_err() {
126 debug!("event receiver dropped");
127 return;
128 }
129 self.dropped = 0;
130 }
131 if self.evt_tx.send(event).await.is_err() {
132 debug!("event receiver dropped");
133 }
134 }
135
136 async fn step_idle(&mut self) -> State {
137 loop {
138 match self.cmd_rx.recv().await {
139 Some(Command::Connect) => {
140 return State::Connecting { attempt: 0 };
141 }
142 None => return State::Done,
143 Some(Command::Disconnect) => {
144 warn!("Command disconnect ignored - not connected");
145 }
146 Some(Command::Send(_)) => {
147 warn!("Send ignored - not connected");
148 }
149 }
150 }
151 }
152
153 async fn step_connecting(&mut self, attempt: u32) -> State {
154 debug!(attempt, "connecting");
155
156 let url = self.config.url.clone();
157 let connect = timeout(self.config.connect_timeout, connect_async(url));
158 tokio::pin!(connect);
159
160 let result = loop {
163 tokio::select! {
164 result = &mut connect => break result,
165 cmd = self.cmd_rx.recv() => match cmd {
166 None | Some(Command::Disconnect) => {
167 info!("disconnect requested while connecting");
168 self.emit_lifecycle(Event::Disconnected {
169 reason: DisconnectReason::Requested,
170 })
171 .await;
172 return State::Idle;
173 }
174 Some(Command::Connect) => debug!("Connect ignored - already connecting"),
175 Some(Command::Send(_)) => warn!("Send ignored - not connected yet"),
176 },
177 }
178 };
179
180 match result {
181 Ok(Ok((ws_stream, _))) => {
182 info!("websocket connected");
183 self.emit_lifecycle(Event::Connected).await;
184
185 let (sink, stream) = ws_stream.split();
186 let (frame_tx, frame_rx) =
187 mpsc::channel::<FrameResult>(self.config.event_queue_size);
188
189 let read_task = tokio::spawn(async move {
190 let mut stream = stream;
191 while let Some(msg) = stream.next().await {
192 if frame_tx.send(msg).await.is_err() {
193 break;
194 }
195 }
196 });
197
198 State::Connected {
199 frame_rx,
200 read_task,
201 sink: Box::new(sink),
202 }
203 }
204 Ok(Err(e)) => {
205 error!(error = %e, attempt, "connection failed");
206 self.next_reconnect_state(attempt + 1, DisconnectReason::Error(e.to_string()))
207 .await
208 }
209 Err(_) => {
210 error!(attempt, "connection timed out");
211 self.next_reconnect_state(
212 attempt + 1,
213 DisconnectReason::Error(String::from("connect timeout")),
214 )
215 .await
216 }
217 }
218 }
219
220 async fn step_connected(
221 &mut self,
222 mut frame_rx: mpsc::Receiver<FrameResult>,
223 read_task: tokio::task::JoinHandle<()>,
224 mut sink: Sink,
225 ) -> State {
226 let ping_interval = self.config.ping_interval;
227 let pong_timeout = self.config.pong_timeout;
228 let mut ping_timer = Box::pin(match ping_interval {
229 Some(d) => sleep(d),
230 None => sleep(FAR_FUTURE),
231 });
232 let mut pong_timer = Box::pin(sleep(FAR_FUTURE));
233 let mut hb = HeartbeatState::Idle;
234
235 loop {
236 tokio::select! {
237 biased;
238
239 frame = frame_rx.recv() => match frame {
240 None => {
241 info!("remote closed the connection");
242 read_task.abort();
243 return self.next_reconnect_state(1, DisconnectReason::RemoteClosed).await;
244 }
245 Some(Ok(msg)) => {
246 match msg {
247 Message::Text(json) => {
248 match IncomingMessage::from_json(&json){
249 Ok(msg) => {
250 if matches!(hb, HeartbeatState::PingSent) && (msg.is_ping() || msg.is_pong()) {
255 debug!("receiving heartbeat pong");
256 hb = HeartbeatState::Idle;
257 pong_timer.as_mut().reset(far_future_instant());
258 if let Some(d) = ping_interval {
259 ping_timer.as_mut().reset(Instant::now() + d);
260 }
261 }
262
263 self.emit(Event::Message(msg));
264 }
265 Err(e) => {
266 warn!(error = %e, "parsing IncomingMessage failed");
267 self.emit(Event::ParseError(e.to_string()));
268 }
269 }
270 }
271 Message::Binary(bytes) => debug!("binary message received ({}B)", bytes.len()),
272 Message::Ping(bytes) => debug!("ping received ({}B)", bytes.len()),
273 Message::Pong(bytes) => debug!("pong received ({}B)", bytes.len()),
274 Message::Close(close_frame) => debug!("close frame received [{:?}]", close_frame),
275 Message::Frame(frame) =>debug!("frame received ({}B)", frame.len()),
276 }
277 }
278 Some(Err(e)) => {
279 error!(error = %e, "websocket read error");
280 read_task.abort();
281 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
282 }
283 },
284
285 cmd = self.cmd_rx.recv() => match cmd {
286 None | Some(Command::Disconnect) => {
287 info!("disconnect requested");
288 return State::Closing { frame_rx, read_task, sink };
289 }
290 Some(Command::Send(msg)) => {
291 let json = match serialize_json(&msg) {
292 Ok(json) => json,
293 Err(e) => {
294 warn!(error = %e, "serializing outgoing message failed, message dropped");
295 continue;
296 }
297 };
298 if let Err(e) = sink.send(Message::Text(json.into())).await {
299 error!(error = %e, "send error");
300 read_task.abort();
301 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
302 }
303 }
304 Some(Command::Connect) => warn!("Connect ignored - already connected")
305 },
306
307 () = &mut ping_timer, if ping_interval.is_some() => {
308 debug!("sending heartbeat ping");
309 if let Err(e) = sink.send(ping()).await {
310 error!(error = %e, "ping send error");
311 read_task.abort();
312 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
313 }
314
315 hb = HeartbeatState::PingSent;
316 pong_timer.as_mut().reset(Instant::now() + pong_timeout);
317 ping_timer.as_mut().reset(far_future_instant());
318 },
319
320 () = &mut pong_timer, if matches!(hb, HeartbeatState::PingSent) => {
321 warn!("pong timeout - connection appears dead");
322 read_task.abort();
323 return self.next_reconnect_state(1, DisconnectReason::PongTimeout).await;
324 },
325 }
326 }
327 }
328
329 async fn step_reconnecting(&mut self, attempt: u32, delay_ms: u64) -> State {
330 warn!(attempt, delay_ms, "waiting before reconnect");
331 self.emit_lifecycle(Event::Reconnecting { attempt, delay_ms })
332 .await;
333
334 let deadline = Instant::now() + Duration::from_millis(delay_ms);
337 loop {
338 tokio::select! {
339 () = sleep_until(deadline) => return State::Connecting { attempt },
340 cmd = self.cmd_rx.recv() => match cmd {
341 None | Some(Command::Disconnect) => {
342 self.emit_lifecycle(Event::Disconnected {
343 reason: DisconnectReason::Requested,
344 })
345 .await;
346 return State::Idle;
347 }
348 Some(Command::Connect) => debug!("Connect ignored - already reconnecting"),
349 Some(Command::Send(_)) => warn!("Send ignored - not connected"),
350 },
351 }
352 }
353 }
354
355 async fn step_closing(
356 &mut self,
357 mut frame_rx: mpsc::Receiver<FrameResult>,
358 read_task: tokio::task::JoinHandle<()>,
359 mut sink: Sink,
360 ) -> State {
361 match sink.send(Message::Close(None)).await {
362 Ok(()) => {
363 let handshake = async {
367 while let Some(frame) = frame_rx.recv().await {
368 if matches!(frame, Ok(Message::Close(_)) | Err(_)) {
369 break;
370 }
371 }
372 };
373 if timeout(self.config.close_timeout, handshake).await.is_err() {
374 warn!("close handshake timed out");
375 }
376 }
377 Err(e) => debug!(error = %e, "send close message failed"),
378 }
379 read_task.abort();
380
381 self.emit_lifecycle(Event::Disconnected {
382 reason: DisconnectReason::Requested,
383 })
384 .await;
385 State::Idle
386 }
387
388 async fn next_reconnect_state(&mut self, next_attempt: u32, reason: DisconnectReason) -> State {
389 match reconnect_delay_ms(&self.config, next_attempt) {
390 Some(delay_ms) => {
391 debug!(next_attempt, delay_ms, ?reason, "scheduling reconnect");
392 State::Reconnecting {
393 attempt: next_attempt,
394 delay_ms,
395 }
396 }
397 None => {
398 warn!(?reason, "giving up reconnecting");
399 self.emit_lifecycle(Event::Disconnected { reason }).await;
400 State::Idle
401 }
402 }
403 }
404}
405
406fn reconnect_delay_ms(config: &Config, attempt: u32) -> Option<u64> {
409 if attempt == 0 || attempt > config.max_reconnect_attempts {
410 return None;
411 }
412 let base_ms = config.reconnect_base_delay.as_millis() as u64;
413 let max_ms = config.reconnect_max_delay.as_millis() as u64;
414 Some(
415 base_ms
416 .saturating_mul(1u64 << (attempt - 1).min(10))
417 .min(max_ms),
418 )
419}
420
421const FAR_FUTURE: Duration = Duration::from_secs(u64::MAX / 4);
422
423#[inline]
424fn far_future_instant() -> Instant {
425 Instant::now() + FAR_FUTURE
426}
427
428#[inline]
430fn ping() -> Message {
431 Message::Text(r#"{"op":"ping"}"#.into())
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::ws::OutgoingMessage;
438 use tokio::net::TcpListener;
439
440 async fn spawn_server() -> String {
443 spawn_server_with_greeting(Vec::new()).await
444 }
445
446 async fn spawn_server_with_greeting(greeting: Vec<String>) -> String {
449 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
450 let addr = listener.local_addr().unwrap();
451 tokio::spawn(async move {
452 while let Ok((tcp, _)) = listener.accept().await {
453 let greeting = greeting.clone();
454 tokio::spawn(async move {
455 let Ok(mut ws) = tokio_tungstenite::accept_async(tcp).await else {
456 return;
457 };
458 for text in greeting {
459 if ws.send(Message::Text(text.into())).await.is_err() {
460 return;
461 }
462 }
463 while let Some(Ok(_)) = ws.next().await {}
464 });
465 }
466 });
467 format!("ws://{addr}")
468 }
469
470 async fn closed_port_url() -> String {
473 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
474 let addr = listener.local_addr().unwrap();
475 drop(listener);
476 format!("ws://{addr}")
477 }
478
479 const EVENT_TIMEOUT: Duration = Duration::from_secs(10);
482
483 async fn next_event(rx: &mut mpsc::Receiver<Event>) -> Event {
484 timeout(EVENT_TIMEOUT, rx.recv())
485 .await
486 .expect("timed out waiting for event")
487 .expect("event channel closed")
488 }
489
490 fn test_config(url: String) -> Config {
491 Config::new(url)
492 .ping_interval(None)
493 .reconnect_base_delay(Duration::from_millis(10))
494 .reconnect_max_delay(Duration::from_millis(20))
495 }
496
497 #[test]
498 fn reconnect_delay_grows_exponentially_and_is_capped() {
499 let config = Config::new("")
500 .max_reconnect_attempts(5)
501 .reconnect_base_delay(Duration::from_millis(100))
502 .reconnect_max_delay(Duration::from_millis(500));
503
504 let delays: Vec<_> = (0..=6).map(|n| reconnect_delay_ms(&config, n)).collect();
505
506 assert_eq!(
507 delays,
508 vec![
509 None,
510 Some(100),
511 Some(200),
512 Some(400),
513 Some(500),
514 Some(500),
515 None
516 ]
517 );
518 }
519
520 #[test]
521 fn reconnect_is_disabled_with_zero_attempts() {
522 let config = Config::new("").max_reconnect_attempts(0);
523
524 assert_eq!(reconnect_delay_ms(&config, 1), None);
525 }
526
527 #[tokio::test]
528 async fn disconnect_completes_without_waiting_for_close_timeout() {
529 let url = spawn_server().await;
530 let (handle, mut events) =
531 Stream::new(test_config(url).close_timeout(Duration::from_secs(30)));
532
533 handle.connect().await.unwrap();
534 assert!(matches!(next_event(&mut events).await, Event::Connected));
535
536 handle.disconnect().await.unwrap();
537 assert!(matches!(
538 next_event(&mut events).await,
539 Event::Disconnected {
540 reason: DisconnectReason::Requested
541 }
542 ));
543 }
544
545 #[tokio::test]
546 async fn connect_right_after_disconnect_is_not_lost() {
547 let url = spawn_server().await;
548 let (handle, mut events) = Stream::new(test_config(url));
549
550 handle.connect().await.unwrap();
551 assert!(matches!(next_event(&mut events).await, Event::Connected));
552
553 handle.disconnect().await.unwrap();
554 handle.connect().await.unwrap();
555 assert!(matches!(
556 next_event(&mut events).await,
557 Event::Disconnected { .. }
558 ));
559 assert!(matches!(next_event(&mut events).await, Event::Connected));
560 }
561
562 #[tokio::test]
563 async fn initial_connect_failure_retries_max_attempts_then_gives_up() {
564 let url = closed_port_url().await;
565 let (handle, mut events) = Stream::new(test_config(url).max_reconnect_attempts(2));
566
567 handle.connect().await.unwrap();
568
569 assert!(matches!(
570 next_event(&mut events).await,
571 Event::Reconnecting { attempt: 1, .. }
572 ));
573 assert!(matches!(
574 next_event(&mut events).await,
575 Event::Reconnecting { attempt: 2, .. }
576 ));
577 assert!(matches!(
578 next_event(&mut events).await,
579 Event::Disconnected {
580 reason: DisconnectReason::Error(_)
581 }
582 ));
583 }
584
585 #[tokio::test]
586 async fn commands_during_backoff_do_not_skip_the_delay() {
587 let url = closed_port_url().await;
588 let config = test_config(url)
589 .max_reconnect_attempts(1)
590 .reconnect_base_delay(Duration::from_millis(300))
591 .reconnect_max_delay(Duration::from_millis(300));
592 let (handle, mut events) = Stream::new(config);
593
594 handle.connect().await.unwrap();
595 assert!(matches!(
596 next_event(&mut events).await,
597 Event::Reconnecting { attempt: 1, .. }
598 ));
599 let started = Instant::now();
600 handle.connect().await.unwrap();
601 handle
602 .send_command(OutgoingMessage::Ping { req_id: None })
603 .await
604 .unwrap();
605
606 assert!(matches!(
607 next_event(&mut events).await,
608 Event::Disconnected { .. }
609 ));
610 assert!(started.elapsed() >= Duration::from_millis(250));
611 }
612
613 #[tokio::test]
614 async fn dropped_data_events_are_reported_before_the_next_lifecycle_event() {
615 let pong = r#"{"op":"pong","conn_id":"test"}"#.to_string();
616 let url = spawn_server_with_greeting(vec![pong; 10]).await;
617 let (handle, mut events) = Stream::new(test_config(url).event_queue_size(2));
618
619 handle.connect().await.unwrap();
620 sleep(Duration::from_millis(300)).await;
622 assert!(matches!(next_event(&mut events).await, Event::Connected));
623 assert!(matches!(next_event(&mut events).await, Event::Message(_)));
624
625 handle.disconnect().await.unwrap();
626
627 assert!(matches!(
628 next_event(&mut events).await,
629 Event::Lagged { dropped: 9 }
630 ));
631 assert!(matches!(
632 next_event(&mut events).await,
633 Event::Disconnected { .. }
634 ));
635 }
636}