Skip to main content

bybit/ws/
stream.rs

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
17/// WebSocket connection driver.
18///
19/// [`Stream::new`] spawns the driver on the current Tokio runtime and returns a
20/// [`Handle`] for commands and a receiver of [`Event`]s.
21///
22/// # Reconnects
23/// After an unexpected disconnect the driver reconnects automatically (see
24/// [`Config::max_reconnect_attempts`]) and emits [`Event::Connected`] again.
25/// Subscriptions and authentication are *not* restored: after every
26/// `Event::Connected` the user must send `auth` (private streams) and
27/// `subscribe` again.
28///
29/// # Event delivery
30/// Lifecycle events (`Connected`, `Reconnecting`, `Disconnected`) are always
31/// delivered: the driver waits for free space in the event queue. Market and
32/// account data (`Message`, `ParseError`) are dropped when the queue is full;
33/// the number of dropped events is reported by [`Event::Lagged`] as soon as
34/// there is room again.
35pub struct Stream {
36    config: Config,
37    cmd_rx: mpsc::Receiver<Command>,
38    evt_tx: mpsc::Sender<Event>,
39    /// Data events dropped since the last [`Event::Lagged`].
40    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    /// Deliver a data event without blocking the driver; drop it (and count
90    /// it) when the event queue is full.
91    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    /// Deliver a lifecycle event, waiting for space in the event queue.
119    /// A pending [`Event::Lagged`] is delivered first to keep the order.
120    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        // Keep serving commands while the handshake is in flight so that a
161        // `Disconnect` is not blocked behind a slow or hanging connection.
162        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                                        // INFO: Bybit heartbeat process.
251                                        // - send to server: { "op": "ping" }
252                                        // - for public channels receive from server: { "op": "ping", "ret_msg": "pong", "success": true }
253                                        // - for private channels receive from server: { "op": "pong" }
254                                        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        // Commands that arrive during the back-off must neither shorten the
335        // delay nor vanish silently, so the deadline is fixed up front.
336        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                // Wait for the server to answer with its own Close frame (or to
364                // drop the connection). Commands are not read here, so nothing
365                // the user sends meanwhile is lost: it is handled in `Idle`.
366                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
406/// Back-off delay before reconnect attempt number `attempt` (1-based), or
407/// `None` when the attempt exceeds `max_reconnect_attempts`.
408fn 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/// Bybit application-level heartbeat (`OutgoingMessage::Ping` without `req_id`).
429#[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    /// Local WebSocket server that accepts any number of connections and
441    /// answers the close handshake. Returns its `ws://` URL.
442    async fn spawn_server() -> String {
443        spawn_server_with_greeting(Vec::new()).await
444    }
445
446    /// Like [`spawn_server`], but sends `greeting` text frames right after
447    /// every handshake.
448    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    /// An address nothing listens on. Connecting to it fails at once on
471    /// Linux and macOS, but takes about 2 s on Windows (SYN retries).
472    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    /// Generous upper bound for a single event; tests do not wait this long
480    /// unless something is wrong (see [`closed_port_url`] for Windows).
481    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        // Let the driver fill the queue (Connected + 1 message) and drop the rest.
621        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}