nautilus_binance/spot/websocket/streams/
handler.rs1use std::{
23 collections::VecDeque,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, AtomicU64, Ordering},
27 },
28};
29
30use ahash::AHashMap;
31use nautilus_network::{
32 RECONNECTED,
33 websocket::{SubscriptionState, WebSocketClient},
34};
35use tokio_tungstenite::tungstenite::Message;
36use ustr::Ustr;
37
38pub use super::parse::{MarketDataMessage, decode_market_data};
39use super::{
40 messages::{
41 BinanceSpotServerShutdownMsg, BinanceSpotWsMessage, BinanceSpotWsStreamsCommand,
42 BinanceWsErrorMsg, BinanceWsErrorResponse, BinanceWsResponse, BinanceWsSubscription,
43 },
44 parse::decode_market_data as decode_sbe,
45};
46use crate::common::consts::BINANCE_RATE_LIMIT_KEY_SUBSCRIPTION;
47
48pub(super) struct BinanceSpotWsFeedHandler {
54 #[allow(dead_code)]
55 signal: Arc<AtomicBool>,
56 inner: Option<WebSocketClient>,
57 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<BinanceSpotWsStreamsCommand>,
58 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
59 #[allow(dead_code)]
60 out_tx: tokio::sync::mpsc::UnboundedSender<BinanceSpotWsMessage>,
61 subscriptions: SubscriptionState,
62 request_id_counter: Arc<AtomicU64>,
63 pending_messages: VecDeque<BinanceSpotWsMessage>,
64 pending_requests: AHashMap<u64, Vec<String>>,
65}
66
67impl BinanceSpotWsFeedHandler {
68 pub(super) fn new(
70 signal: Arc<AtomicBool>,
71 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<BinanceSpotWsStreamsCommand>,
72 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
73 out_tx: tokio::sync::mpsc::UnboundedSender<BinanceSpotWsMessage>,
74 subscriptions: SubscriptionState,
75 request_id_counter: Arc<AtomicU64>,
76 ) -> Self {
77 Self {
78 signal,
79 inner: None,
80 cmd_rx,
81 raw_rx,
82 out_tx,
83 subscriptions,
84 request_id_counter,
85 pending_messages: VecDeque::new(),
86 pending_requests: AHashMap::new(),
87 }
88 }
89
90 pub(super) async fn next(&mut self) -> Option<BinanceSpotWsMessage> {
94 if let Some(message) = self.pending_messages.pop_front() {
95 return Some(message);
96 }
97
98 loop {
99 tokio::select! {
100 Some(cmd) = self.cmd_rx.recv() => {
101 match cmd {
102 BinanceSpotWsStreamsCommand::SetClient(client) => {
103 log::debug!("Handler received WebSocket client");
104 self.inner = Some(client);
105 }
106 BinanceSpotWsStreamsCommand::Disconnect => {
107 log::debug!("Handler disconnecting WebSocket client");
108 self.inner = None;
109 return None;
110 }
111 BinanceSpotWsStreamsCommand::Subscribe { streams } => {
112 if let Err(e) = self.handle_subscribe(streams).await {
113 log::error!("Failed to handle subscribe command: {e}");
114 }
115 }
116 BinanceSpotWsStreamsCommand::Unsubscribe { streams } => {
117 if let Err(e) = self.handle_unsubscribe(streams).await {
118 log::error!("Failed to handle unsubscribe command: {e}");
119 }
120 }
121 }
122 }
123 Some(msg) = self.raw_rx.recv() => {
124 if let Message::Text(ref text) = msg
125 && text.as_str() == RECONNECTED
126 {
127 log::debug!("Handler received reconnection signal");
128 return Some(BinanceSpotWsMessage::Reconnected);
129 }
130
131 let messages = self.handle_message(msg);
132 if !messages.is_empty() {
133 let mut iter = messages.into_iter();
134 let first = iter.next();
135 self.pending_messages.extend(iter);
136
137 if let Some(msg) = first {
138 return Some(msg);
139 }
140 }
141 }
142 else => {
143 return None;
144 }
145 }
146 }
147 }
148
149 fn handle_message(&mut self, msg: Message) -> Vec<BinanceSpotWsMessage> {
150 match msg {
151 Message::Binary(data) => self.handle_binary_frame(&data),
152 Message::Text(text) => self.handle_text_frame(&text),
153 Message::Close(_) => {
154 log::debug!("Received close frame");
155 vec![]
156 }
157 Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => vec![],
158 }
159 }
160
161 fn handle_binary_frame(&self, data: &[u8]) -> Vec<BinanceSpotWsMessage> {
162 match decode_sbe(data) {
163 Ok(MarketDataMessage::Trades(event)) => {
164 vec![BinanceSpotWsMessage::Trades(event)]
165 }
166 Ok(MarketDataMessage::BestBidAsk(event)) => {
167 vec![BinanceSpotWsMessage::BestBidAsk(event)]
168 }
169 Ok(MarketDataMessage::DepthSnapshot(event)) => {
170 vec![BinanceSpotWsMessage::DepthSnapshot(event)]
171 }
172 Ok(MarketDataMessage::DepthDiff(event)) => {
173 vec![BinanceSpotWsMessage::DepthDiff(event)]
174 }
175 Err(e) => {
176 log::error!("SBE decode error: {e}");
177 vec![BinanceSpotWsMessage::RawBinary(data.to_vec())]
178 }
179 }
180 }
181
182 fn handle_text_frame(&mut self, text: &str) -> Vec<BinanceSpotWsMessage> {
183 if let Ok(error) = serde_json::from_str::<BinanceWsErrorResponse>(text) {
184 if let Some(id) = error.id
185 && let Some(streams) = self.pending_requests.remove(&id)
186 {
187 for stream in &streams {
188 self.subscriptions.mark_failure(stream);
189 }
190 log::warn!(
191 "Subscription request failed: id={id}, streams={streams:?}, code={}, msg={}",
192 error.code,
193 error.msg
194 );
195 }
196 return vec![BinanceSpotWsMessage::Error(BinanceWsErrorMsg {
197 code: error.code,
198 msg: error.msg,
199 })];
200 }
201
202 if let Ok(response) = serde_json::from_str::<BinanceWsResponse>(text) {
203 self.handle_subscription_response(&response);
204 return vec![];
205 }
206
207 classify_unsolicited_json(text)
208 }
209
210 fn handle_subscription_response(&mut self, response: &BinanceWsResponse) {
211 if let Some(streams) = self.pending_requests.remove(&response.id) {
212 if response.result.is_none() {
213 for stream in &streams {
214 self.subscriptions.confirm_subscribe(stream);
215 }
216 log::debug!("Subscription confirmed: streams={streams:?}");
217 } else {
218 for stream in &streams {
219 self.subscriptions.mark_failure(stream);
220 }
221 log::warn!(
222 "Subscription failed: streams={streams:?}, result={:?}",
223 response.result
224 );
225 }
226 } else {
227 log::debug!("Received response for unknown request: id={}", response.id);
228 }
229 }
230
231 async fn handle_subscribe(&mut self, streams: Vec<String>) -> anyhow::Result<()> {
232 let request_id = self.request_id_counter.fetch_add(1, Ordering::SeqCst);
233 let request = BinanceWsSubscription::subscribe(streams.clone(), request_id);
234 let payload = serde_json::to_string(&request)?;
235
236 self.pending_requests.insert(request_id, streams.clone());
237
238 for stream in &streams {
239 self.subscriptions.mark_subscribe(stream);
240 }
241
242 self.send_text(
243 payload,
244 Some(BINANCE_RATE_LIMIT_KEY_SUBSCRIPTION.as_slice()),
245 )
246 .await?;
247 Ok(())
248 }
249
250 async fn handle_unsubscribe(&self, streams: Vec<String>) -> anyhow::Result<()> {
251 let request_id = self.request_id_counter.fetch_add(1, Ordering::SeqCst);
252 let request = BinanceWsSubscription::unsubscribe(streams.clone(), request_id);
253 let payload = serde_json::to_string(&request)?;
254
255 self.send_text(
256 payload,
257 Some(BINANCE_RATE_LIMIT_KEY_SUBSCRIPTION.as_slice()),
258 )
259 .await?;
260
261 for stream in &streams {
262 self.subscriptions.mark_unsubscribe(stream);
263 self.subscriptions.confirm_unsubscribe(stream);
264 }
265
266 Ok(())
267 }
268
269 async fn send_text(
270 &self,
271 payload: String,
272 rate_limit_keys: Option<&[Ustr]>,
273 ) -> anyhow::Result<()> {
274 let Some(client) = &self.inner else {
275 anyhow::bail!("No active WebSocket client");
276 };
277 client
278 .send_text(payload, rate_limit_keys)
279 .await
280 .map_err(|e| anyhow::anyhow!("Failed to send message: {e}"))?;
281 Ok(())
282 }
283}
284
285fn classify_unsolicited_json(text: &str) -> Vec<BinanceSpotWsMessage> {
289 let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
290 log::warn!("Failed to parse JSON message: {text}");
291 return vec![];
292 };
293
294 if value.get("e").and_then(|v| v.as_str()) == Some("serverShutdown")
295 && let Ok(msg) = serde_json::from_value::<BinanceSpotServerShutdownMsg>(value.clone())
296 {
297 log::warn!(
298 "Binance server shutdown notice received (event_time={}); disconnect expected ~10 minutes from event",
299 msg.event_time,
300 );
301 return vec![BinanceSpotWsMessage::ServerShutdown(msg)];
302 }
303
304 vec![BinanceSpotWsMessage::RawJson(value)]
305}
306
307#[cfg(test)]
308mod tests {
309 use std::sync::{
310 Arc,
311 atomic::{AtomicBool, AtomicU64},
312 };
313
314 use nautilus_network::websocket::SubscriptionState;
315 use rstest::rstest;
316
317 use super::*;
318
319 #[rstest]
320 fn test_classify_unsolicited_json_server_shutdown_emits_variant() {
321 let text = r#"{"e":"serverShutdown","E":1700000000000}"#;
322 let out = classify_unsolicited_json(text);
323 assert_eq!(out.len(), 1);
324 match &out[0] {
325 BinanceSpotWsMessage::ServerShutdown(msg) => {
326 assert_eq!(msg.event_type, "serverShutdown");
327 assert_eq!(msg.event_time, 1_700_000_000_000);
328 }
329 other => panic!("expected ServerShutdown variant, was {other:?}"),
330 }
331 }
332
333 #[rstest]
334 fn test_classify_unsolicited_json_unrelated_emits_raw_json() {
335 let text = r#"{"e":"trade","p":"50000"}"#;
336 let out = classify_unsolicited_json(text);
337 assert_eq!(out.len(), 1);
338 assert!(matches!(out[0], BinanceSpotWsMessage::RawJson(_)));
339 }
340
341 #[rstest]
342 fn test_classify_unsolicited_json_invalid_returns_empty() {
343 let out = classify_unsolicited_json("not json");
344 assert!(out.is_empty());
345 }
346
347 #[rstest]
348 fn test_handle_text_frame_error_with_id_emits_error_and_clears_pending_request() {
349 let signal = Arc::new(AtomicBool::new(false));
350 let request_id_counter = Arc::new(AtomicU64::new(2));
351 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
352 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
353 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
354 let subscriptions = SubscriptionState::new('@');
355
356 let mut handler = BinanceSpotWsFeedHandler::new(
357 signal,
358 cmd_rx,
359 raw_rx,
360 out_tx,
361 subscriptions,
362 request_id_counter,
363 );
364 handler
365 .pending_requests
366 .insert(1, vec!["btcusdt@trade".to_string()]);
367
368 let out = handler.handle_text_frame(r#"{"code":2,"msg":"Invalid request","id":1}"#);
369 assert_eq!(out.len(), 1);
370 match &out[0] {
371 BinanceSpotWsMessage::Error(err) => {
372 assert_eq!(err.code, 2);
373 assert_eq!(err.msg, "Invalid request");
374 }
375 other => panic!("expected Error variant, was {other:?}"),
376 }
377 assert!(handler.pending_requests.is_empty());
378 }
379}