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.
471    async fn closed_port_url() -> String {
472        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
473        let addr = listener.local_addr().unwrap();
474        drop(listener);
475        format!("ws://{addr}")
476    }
477
478    async fn next_event(rx: &mut mpsc::Receiver<Event>) -> Event {
479        timeout(Duration::from_secs(2), rx.recv())
480            .await
481            .expect("timed out waiting for event")
482            .expect("event channel closed")
483    }
484
485    fn test_config(url: String) -> Config {
486        Config::new(url)
487            .ping_interval(None)
488            .reconnect_base_delay(Duration::from_millis(10))
489            .reconnect_max_delay(Duration::from_millis(20))
490    }
491
492    #[test]
493    fn reconnect_delay_grows_exponentially_and_is_capped() {
494        let config = Config::new("")
495            .max_reconnect_attempts(5)
496            .reconnect_base_delay(Duration::from_millis(100))
497            .reconnect_max_delay(Duration::from_millis(500));
498
499        let delays: Vec<_> = (0..=6).map(|n| reconnect_delay_ms(&config, n)).collect();
500
501        assert_eq!(
502            delays,
503            vec![
504                None,
505                Some(100),
506                Some(200),
507                Some(400),
508                Some(500),
509                Some(500),
510                None
511            ]
512        );
513    }
514
515    #[test]
516    fn reconnect_is_disabled_with_zero_attempts() {
517        let config = Config::new("").max_reconnect_attempts(0);
518
519        assert_eq!(reconnect_delay_ms(&config, 1), None);
520    }
521
522    #[tokio::test]
523    async fn disconnect_completes_without_waiting_for_close_timeout() {
524        let url = spawn_server().await;
525        let (handle, mut events) =
526            Stream::new(test_config(url).close_timeout(Duration::from_secs(30)));
527
528        handle.connect().await.unwrap();
529        assert!(matches!(next_event(&mut events).await, Event::Connected));
530
531        handle.disconnect().await.unwrap();
532        assert!(matches!(
533            next_event(&mut events).await,
534            Event::Disconnected {
535                reason: DisconnectReason::Requested
536            }
537        ));
538    }
539
540    #[tokio::test]
541    async fn connect_right_after_disconnect_is_not_lost() {
542        let url = spawn_server().await;
543        let (handle, mut events) = Stream::new(test_config(url));
544
545        handle.connect().await.unwrap();
546        assert!(matches!(next_event(&mut events).await, Event::Connected));
547
548        handle.disconnect().await.unwrap();
549        handle.connect().await.unwrap();
550        assert!(matches!(
551            next_event(&mut events).await,
552            Event::Disconnected { .. }
553        ));
554        assert!(matches!(next_event(&mut events).await, Event::Connected));
555    }
556
557    #[tokio::test]
558    async fn initial_connect_failure_retries_max_attempts_then_gives_up() {
559        let url = closed_port_url().await;
560        let (handle, mut events) = Stream::new(test_config(url).max_reconnect_attempts(2));
561
562        handle.connect().await.unwrap();
563
564        assert!(matches!(
565            next_event(&mut events).await,
566            Event::Reconnecting { attempt: 1, .. }
567        ));
568        assert!(matches!(
569            next_event(&mut events).await,
570            Event::Reconnecting { attempt: 2, .. }
571        ));
572        assert!(matches!(
573            next_event(&mut events).await,
574            Event::Disconnected {
575                reason: DisconnectReason::Error(_)
576            }
577        ));
578    }
579
580    #[tokio::test]
581    async fn commands_during_backoff_do_not_skip_the_delay() {
582        let url = closed_port_url().await;
583        let config = test_config(url)
584            .max_reconnect_attempts(1)
585            .reconnect_base_delay(Duration::from_millis(300))
586            .reconnect_max_delay(Duration::from_millis(300));
587        let (handle, mut events) = Stream::new(config);
588
589        handle.connect().await.unwrap();
590        assert!(matches!(
591            next_event(&mut events).await,
592            Event::Reconnecting { attempt: 1, .. }
593        ));
594        let started = Instant::now();
595        handle.connect().await.unwrap();
596        handle
597            .send_command(OutgoingMessage::Ping { req_id: None })
598            .await
599            .unwrap();
600
601        assert!(matches!(
602            next_event(&mut events).await,
603            Event::Disconnected { .. }
604        ));
605        assert!(started.elapsed() >= Duration::from_millis(250));
606    }
607
608    #[tokio::test]
609    async fn dropped_data_events_are_reported_before_the_next_lifecycle_event() {
610        let pong = r#"{"op":"pong","conn_id":"test"}"#.to_string();
611        let url = spawn_server_with_greeting(vec![pong; 10]).await;
612        let (handle, mut events) = Stream::new(test_config(url).event_queue_size(2));
613
614        handle.connect().await.unwrap();
615        // Let the driver fill the queue (Connected + 1 message) and drop the rest.
616        sleep(Duration::from_millis(300)).await;
617        assert!(matches!(next_event(&mut events).await, Event::Connected));
618        assert!(matches!(next_event(&mut events).await, Event::Message(_)));
619
620        handle.disconnect().await.unwrap();
621
622        assert!(matches!(
623            next_event(&mut events).await,
624            Event::Lagged { dropped: 9 }
625        ));
626        assert!(matches!(
627            next_event(&mut events).await,
628            Event::Disconnected { .. }
629        ));
630    }
631}