1use std::{
28 fmt::Debug,
29 sync::{
30 Arc,
31 atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
32 },
33};
34
35use futures_util::Stream;
36use nautilus_core::{
37 AtomicMap,
38 string::secret::{REDACTED, SecretString},
39};
40use nautilus_live::{
41 SocketControl, SocketControlFactory,
42 task::{TaskJoinOutcome, TaskSlot, finish_task},
43};
44use nautilus_model::instruments::{Instrument, InstrumentAny};
45use nautilus_network::{
46 http::create_standard_nautilus_headers,
47 mode::ConnectionMode,
48 websocket::{
49 PingHandler, SubscriptionState, TransportBackend, WebSocketClient, WebSocketConfig,
50 channel_message_handler,
51 },
52};
53use parking_lot::Mutex;
54use tokio_tungstenite::tungstenite::Message;
55use tokio_util::sync::CancellationToken;
56use ustr::Ustr;
57
58use super::{
59 error::{BinanceWsError, BinanceWsResult},
60 handler::BinanceFuturesDataWsFeedHandler,
61 messages::{BinanceFuturesWsStreamsCommand, BinanceFuturesWsStreamsMessage},
62};
63use crate::common::{
64 consts::{
65 BINANCE_API_KEY_HEADER, BINANCE_RATE_LIMIT_KEY_SUBSCRIPTION, BINANCE_WS_CONNECTION_QUOTA,
66 BINANCE_WS_SUBSCRIPTION_QUOTA,
67 },
68 credential::SigningCredential,
69 enums::{BinanceEnvironment, BinanceProductType},
70 urls::get_ws_base_url,
71};
72
73pub const MAX_STREAMS_PER_CONNECTION: usize = 200;
75
76const MAX_CONNECTIONS: usize = 20;
78
79struct ConnectionSlot {
81 cmd_tx: tokio::sync::mpsc::UnboundedSender<BinanceFuturesWsStreamsCommand>,
82 streams: Vec<String>,
83 subscriptions_state: SubscriptionState,
84 handler_task: TaskSlot<()>,
85 bytes_task: TaskSlot<()>,
86 cancellation_token: CancellationToken,
87 connection_mode: Arc<AtomicU8>,
88 socket_control: Option<SocketControl>,
89 shutdown_errors: Vec<String>,
90}
91
92#[derive(Clone)]
99pub struct BinanceFuturesWebSocketClient {
100 url: SecretString,
101 product_type: BinanceProductType,
102 credential: Option<Arc<SigningCredential>>,
103 heartbeat: Option<u64>,
104 signal: Arc<AtomicBool>,
105 slots: Arc<ConnectionSlots>,
106 connect_lock: Arc<tokio::sync::Mutex<()>>,
107 out_tx: Arc<Mutex<Option<tokio::sync::mpsc::UnboundedSender<BinanceFuturesWsStreamsMessage>>>>,
108 out_rx:
109 Arc<Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<BinanceFuturesWsStreamsMessage>>>>,
110 request_id_counter: Arc<AtomicU64>,
111 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
112 transport_backend: TransportBackend,
113 proxy_url: Option<SecretString>,
114 socket_factory: Option<SocketControlFactory>,
115 socket_endpoint: Option<String>,
116}
117
118impl Debug for BinanceFuturesWebSocketClient {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct(stringify!(BinanceFuturesWebSocketClient))
121 .field("url", &REDACTED)
122 .field("product_type", &self.product_type)
123 .field("credential", &self.credential.as_ref().map(|_| REDACTED))
124 .field("heartbeat", &self.heartbeat)
125 .finish_non_exhaustive()
126 }
127}
128
129impl BinanceFuturesWebSocketClient {
130 pub fn new(
138 product_type: BinanceProductType,
139 environment: BinanceEnvironment,
140 api_key: Option<String>,
141 api_secret: Option<String>,
142 url_override: Option<String>,
143 heartbeat: Option<u64>,
144 transport_backend: TransportBackend,
145 ) -> anyhow::Result<Self> {
146 match product_type {
147 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
148 _ => {
149 anyhow::bail!(
150 "BinanceFuturesWebSocketClient requires UsdM or CoinM product type, was {product_type:?}"
151 );
152 }
153 }
154
155 let url = SecretString::from(
156 url_override.unwrap_or_else(|| get_ws_base_url(product_type, environment).to_string()),
157 );
158
159 let credential = match (api_key, api_secret) {
160 (Some(key), Some(secret)) => Some(Arc::new(SigningCredential::new(key, secret))),
161 _ => None,
162 };
163
164 Ok(Self {
165 url,
166 product_type,
167 credential,
168 heartbeat,
169 signal: Arc::new(AtomicBool::new(false)),
170 slots: Arc::new(ConnectionSlots(Mutex::new(Vec::new()))),
171 connect_lock: Arc::new(tokio::sync::Mutex::new(())),
172 out_tx: Arc::new(Mutex::new(None)),
173 out_rx: Arc::new(Mutex::new(None)),
174 request_id_counter: Arc::new(AtomicU64::new(1)),
175 instruments_cache: Arc::new(AtomicMap::new()),
176 transport_backend,
177 proxy_url: None,
178 socket_factory: None,
179 socket_endpoint: None,
180 })
181 }
182
183 #[must_use]
185 pub fn with_proxy(mut self, proxy_url: Option<String>) -> Self {
186 self.proxy_url = proxy_url.map(SecretString::from);
187 self
188 }
189
190 #[must_use]
192 pub fn with_socket_control(
193 mut self,
194 factory: SocketControlFactory,
195 endpoint: impl Into<String>,
196 ) -> Self {
197 self.socket_factory = Some(factory);
198 self.socket_endpoint = Some(endpoint.into());
199 self
200 }
201
202 #[must_use]
204 pub const fn product_type(&self) -> BinanceProductType {
205 self.product_type
206 }
207
208 #[must_use]
210 pub fn is_active(&self) -> bool {
211 let slots = self.slots.lock();
212 slots
213 .iter()
214 .any(|s| s.connection_mode.load(Ordering::Relaxed) == ConnectionMode::Active as u8)
215 }
216
217 #[must_use]
219 pub fn is_closed(&self) -> bool {
220 let slots = self.slots.lock();
221 slots.is_empty()
222 || slots
223 .iter()
224 .all(|s| s.connection_mode.load(Ordering::Relaxed) == ConnectionMode::Closed as u8)
225 }
226
227 #[must_use]
229 pub fn subscription_count(&self) -> usize {
230 let slots = self.slots.lock();
231 slots.iter().map(|s| s.subscriptions_state.len()).sum()
232 }
233
234 pub async fn connect(&mut self) -> BinanceWsResult<()> {
240 let connect_lock = Arc::clone(&self.connect_lock);
241 let _connect_guard = connect_lock.lock().await;
242
243 if !self.slots.lock().is_empty() {
244 self.close_connections().await?;
245 }
246
247 {
248 let _slots = self.slots.lock();
249 self.signal.store(false, Ordering::Release);
250 }
251
252 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel();
253 *self.out_tx.lock() = Some(out_tx);
254 *self.out_rx.lock() = Some(out_rx);
255
256 let slot = self.create_connection(0).await?;
257 let shutdown = {
258 let mut slots = self.slots.lock();
259 let shutdown = self.signal.load(Ordering::Acquire);
260 slots.push(slot);
261 shutdown
262 };
263
264 if shutdown {
265 let rollback = self.close_connections().await;
266 return Err(BinanceWsError::ClientError(match rollback {
267 Ok(()) => "Binance Futures stream pool shutdown began during connect".to_string(),
268 Err(e) => format!(
269 "Binance Futures stream pool shutdown began during connect; rollback failed: {e}"
270 ),
271 }));
272 }
273
274 log::debug!(
275 "Connected to Binance Futures stream pool: product_type={:?}",
276 self.product_type
277 );
278 Ok(())
279 }
280
281 pub async fn close(&mut self) -> BinanceWsResult<()> {
287 self.begin_shutdown();
288 let connect_lock = Arc::clone(&self.connect_lock);
289 let _connect_guard = connect_lock.lock().await;
290 self.close_connections().await
291 }
292
293 pub(crate) fn begin_shutdown(&self) {
294 let slots = self.slots.lock();
295 self.signal.store(true, Ordering::Release);
296
297 for slot in slots.iter() {
298 if let Some(control) = &slot.socket_control {
299 control.deregister();
300 }
301 slot.cancellation_token.cancel();
302 let _ = slot.cmd_tx.send(BinanceFuturesWsStreamsCommand::Disconnect);
303 }
304 }
305
306 async fn close_connections(&self) -> BinanceWsResult<()> {
307 self.begin_shutdown();
308
309 let mut batch = ConnectionSlotBatch::take(&self.slots);
310 let mut index = batch.slots.len();
311 while index > 0 {
312 index -= 1;
313 let slot = &mut batch.slots[index];
314 if let Some(control) = &slot.socket_control {
315 control.deregister();
316 }
317 slot.cancellation_token.cancel();
318 let _ = slot.cmd_tx.send(BinanceFuturesWsStreamsCommand::Disconnect);
319 if let Some(error) =
320 finish_slot_task(&mut slot.handler_task, "Futures stream handler").await
321 {
322 slot.shutdown_errors.push(error);
323 }
324
325 if let Some(error) = finish_slot_task(&mut slot.bytes_task, "Futures byte stream").await
326 {
327 slot.shutdown_errors.push(error);
328 }
329
330 if slot.handler_task.is_none()
331 && slot.bytes_task.is_none()
332 && slot.shutdown_errors.is_empty()
333 {
334 batch.slots.remove(index);
335 }
336 }
337
338 *self.out_tx.lock() = None;
339 *self.out_rx.lock() = None;
340
341 let errors = batch
342 .slots
343 .iter_mut()
344 .flat_map(|slot| std::mem::take(&mut slot.shutdown_errors))
345 .collect::<Vec<_>>();
346 batch
347 .slots
348 .retain(|slot| slot.handler_task.is_some() || slot.bytes_task.is_some());
349
350 if !errors.is_empty() {
351 return Err(BinanceWsError::ClientError(errors.join("; ")));
352 }
353 log::debug!("Disconnected from Binance Futures stream pool");
354 Ok(())
355 }
356
357 pub async fn subscribe(&self, streams: Vec<String>) -> BinanceWsResult<()> {
367 let _connect_guard = self.connect_lock.lock().await;
370
371 let new_streams: Vec<String> = {
373 let slots = self.slots.lock();
374
375 if self.signal.load(Ordering::Acquire) {
376 return Err(BinanceWsError::ClientError(
377 "Binance Futures stream pool is shutting down".to_string(),
378 ));
379 }
380 streams
381 .into_iter()
382 .filter(|s| !slots.iter().any(|slot| slot.streams.contains(s)))
383 .collect()
384 };
385
386 if new_streams.is_empty() {
387 return Ok(());
388 }
389
390 loop {
393 let (remaining_capacity, slot_count) = {
394 let slots = self.slots.lock();
395 let cap: usize = slots
396 .iter()
397 .map(|s| MAX_STREAMS_PER_CONNECTION - s.streams.len())
398 .sum();
399 (cap, slots.len())
400 };
401
402 if remaining_capacity >= new_streams.len() || slot_count >= MAX_CONNECTIONS {
403 break;
404 }
405
406 let new_slot = self.create_connection(slot_count).await?;
407 let (slot_count, shutdown) = {
408 let mut slots = self.slots.lock();
409 let shutdown = self.signal.load(Ordering::Acquire);
410 slots.push(new_slot);
411 (slots.len(), shutdown)
412 };
413
414 if shutdown {
415 let client = self.clone();
416 let rollback = client.close_connections().await;
417 return Err(BinanceWsError::ClientError(match rollback {
418 Ok(()) => {
419 "Binance Futures stream pool shutdown began during subscribe".to_string()
420 }
421 Err(e) => format!(
422 "Binance Futures stream pool shutdown began during subscribe; rollback failed: {e}"
423 ),
424 }));
425 }
426 log::debug!(
427 "Pool slot {} connected: product_type={:?}",
428 slot_count - 1,
429 self.product_type
430 );
431 }
432
433 let mut slots = self.slots.lock();
436
437 if self.signal.load(Ordering::Acquire) {
438 return Err(BinanceWsError::ClientError(
439 "Binance Futures stream pool is shutting down".to_string(),
440 ));
441 }
442 let mut slot_batches: Vec<(usize, Vec<String>)> = Vec::new();
443 let mut slot_counts: Vec<usize> = slots.iter().map(|s| s.streams.len()).collect();
444
445 for stream in &new_streams {
446 let slot_idx = slot_counts
447 .iter()
448 .position(|&count| count < MAX_STREAMS_PER_CONNECTION)
449 .ok_or_else(|| {
450 let max_total = MAX_CONNECTIONS * MAX_STREAMS_PER_CONNECTION;
451 BinanceWsError::ClientError(format!(
452 "Pool exhausted: {max_total} total subscriptions \
453 ({MAX_CONNECTIONS} connections x {MAX_STREAMS_PER_CONNECTION} streams)"
454 ))
455 })?;
456
457 slot_counts[slot_idx] += 1;
458
459 if let Some(batch) = slot_batches.iter_mut().find(|(i, _)| *i == slot_idx) {
460 batch.1.push(stream.clone());
461 } else {
462 slot_batches.push((slot_idx, vec![stream.clone()]));
463 }
464 }
465
466 for (slot_idx, batch) in &slot_batches {
468 slots[*slot_idx]
469 .cmd_tx
470 .send(BinanceFuturesWsStreamsCommand::Subscribe {
471 streams: batch.clone(),
472 })
473 .map_err(|e| {
474 BinanceWsError::ClientError(format!(
475 "Handler not available for pool slot {slot_idx}: {e}"
476 ))
477 })?;
478 slots[*slot_idx].streams.extend(batch.iter().cloned());
479 }
480
481 Ok(())
482 }
483
484 pub async fn unsubscribe(&self, streams: Vec<String>) -> BinanceWsResult<Vec<String>> {
493 self.unsubscribe_inner(streams, None).await
494 }
495
496 pub(crate) async fn unsubscribe_correlated(
498 &self,
499 streams: Vec<String>,
500 correlation: u64,
501 ) -> BinanceWsResult<Vec<String>> {
502 self.unsubscribe_inner(streams, Some(correlation)).await
503 }
504
505 async fn unsubscribe_inner(
506 &self,
507 streams: Vec<String>,
508 correlation: Option<u64>,
509 ) -> BinanceWsResult<Vec<String>> {
510 let _connect_guard = self.connect_lock.lock().await;
511 let mut slots = self.slots.lock();
512
513 if self.signal.load(Ordering::Acquire) {
514 return Err(BinanceWsError::ClientError(
515 "Binance Futures stream pool is shutting down".to_string(),
516 ));
517 }
518 let mut slot_batches: Vec<(usize, Vec<String>)> = Vec::new();
519
520 for stream in &streams {
521 if let Some(slot_idx) = slots.iter().position(|s| s.streams.contains(stream)) {
522 if let Some(batch) = slot_batches.iter_mut().find(|(i, _)| *i == slot_idx) {
523 batch.1.push(stream.clone());
524 } else {
525 slot_batches.push((slot_idx, vec![stream.clone()]));
526 }
527 }
528 }
529
530 for (slot_idx, batch) in &slot_batches {
532 slots[*slot_idx]
533 .cmd_tx
534 .send(BinanceFuturesWsStreamsCommand::Unsubscribe {
535 streams: batch.clone(),
536 correlation,
537 })
538 .map_err(|e| {
539 BinanceWsError::ClientError(format!(
540 "Handler not available for pool slot {slot_idx}: {e}"
541 ))
542 })?;
543
544 for stream in batch {
545 slots[*slot_idx].streams.retain(|s| s != stream);
546 }
547 }
548
549 Ok(slot_batches
550 .into_iter()
551 .flat_map(|(_, batch)| batch)
552 .collect())
553 }
554
555 pub fn stream(&self) -> impl Stream<Item = BinanceFuturesWsStreamsMessage> + 'static {
560 let out_rx = self.out_rx.lock().take();
561 async_stream::stream! {
562 if let Some(mut rx) = out_rx {
563 while let Some(msg) = rx.recv().await {
564 yield msg;
565 }
566 }
567 }
568 }
569
570 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
572 self.instruments_cache.rcu(|m| {
573 for inst in instruments {
574 m.insert(inst.raw_symbol().inner(), inst.clone());
575 }
576 });
577 }
578
579 pub fn replace_instruments(&self, instruments: &[InstrumentAny]) {
581 let cache = instruments
582 .iter()
583 .map(|instrument| (instrument.raw_symbol().inner(), instrument.clone()))
584 .collect();
585 self.instruments_cache.store(cache);
586 }
587
588 pub fn cache_instrument(&self, instrument: InstrumentAny) {
590 self.instruments_cache
591 .insert(instrument.raw_symbol().inner(), instrument);
592 }
593
594 #[must_use]
596 pub fn instruments_cache(&self) -> Arc<AtomicMap<Ustr, InstrumentAny>> {
597 self.instruments_cache.clone()
598 }
599
600 #[must_use]
602 pub fn get_instrument(&self, symbol: &str) -> Option<InstrumentAny> {
603 self.instruments_cache.get_cloned(&Ustr::from(symbol))
604 }
605
606 async fn create_connection(&self, slot_index: usize) -> BinanceWsResult<ConnectionSlot> {
607 let out_tx = self.out_tx.lock().clone().ok_or_else(|| {
608 BinanceWsError::ClientError("Output channel not initialized".to_string())
609 })?;
610
611 let (raw_handler, raw_rx) = channel_message_handler();
612 let ping_handler: PingHandler = Arc::new(move |_| {});
613
614 let mut headers = create_standard_nautilus_headers();
615
616 if let Some(ref cred) = self.credential {
617 headers.push((
618 BINANCE_API_KEY_HEADER.to_string(),
619 cred.api_key().to_string(),
620 ));
621 }
622
623 let config = WebSocketConfig {
624 url: self.url.expose_secret().to_owned(),
625 headers,
626 heartbeat_interval_secs: self.heartbeat,
627 heartbeat_payload: None,
628 connect_timeout_ms: Some(5_000),
629 reconnect_delay_initial_ms: Some(500),
630 reconnect_delay_max_ms: Some(5_000),
631 reconnect_backoff_factor: Some(2.0),
632 reconnect_jitter_ms: Some(250),
633 reconnect_max_attempts: None,
634 heartbeat_timeout_secs: None,
635 idle_timeout_ms: None,
636 backend: self.transport_backend,
637 proxy_url: self
638 .proxy_url
639 .as_ref()
640 .map(|value| value.expose_secret().to_owned()),
641 };
642
643 let keyed_quotas = vec![(
644 BINANCE_RATE_LIMIT_KEY_SUBSCRIPTION[0].to_string(),
645 *BINANCE_WS_SUBSCRIPTION_QUOTA,
646 )];
647
648 let socket_control = self
649 .socket_factory
650 .as_ref()
651 .zip(self.socket_endpoint.as_ref())
652 .map(|(factory, endpoint)| {
653 let endpoint = if slot_index == 0 {
654 endpoint.clone()
655 } else {
656 format!("{endpoint}-{slot_index}")
657 };
658 factory.control(endpoint)
659 });
660 let client = WebSocketClient::builder()
661 .config(config)
662 .message_handler(raw_handler)
663 .ping_handler(ping_handler)
664 .keyed_quotas(keyed_quotas)
665 .default_quota(*BINANCE_WS_CONNECTION_QUOTA)
666 .maybe_state_sink(socket_control.as_ref().map(SocketControl::sink))
667 .connect()
668 .await
669 .map_err(|e| BinanceWsError::NetworkError(e.to_string()))?;
670
671 let connection_mode = client.connection_mode_atomic();
672 let reconnect_handle = client.reconnect_handle();
673 let subscriptions_state = SubscriptionState::new('@');
674 let cancellation_token = CancellationToken::new();
675
676 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
677
678 let (bytes_tx, bytes_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
680
681 let mut bytes_task = TaskSlot::new();
682 if let Err(e) = bytes_task.spawn(async move {
683 let mut raw_rx = raw_rx;
684 while let Some(msg) = raw_rx.recv().await {
685 let data = match msg {
686 Message::Binary(data) => data.to_vec(),
687 Message::Text(text) => text.as_bytes().to_vec(),
688 Message::Close(_) => break,
689 Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => continue,
690 };
691
692 if bytes_tx.send(data).is_err() {
693 break;
694 }
695 }
696 }) {
697 let shutdown_error =
698 finish_slot_task(&mut bytes_task, "Binance futures WS bytes").await;
699 return Err(BinanceWsError::ClientError(match shutdown_error {
700 Some(shutdown_error) => format!(
701 "Failed to start futures WS bytes task: {e}; startup rollback failed: \
702 {shutdown_error}"
703 ),
704 None => format!("Failed to start futures WS bytes task: {e}"),
705 }));
706 }
707
708 let mut handler = BinanceFuturesDataWsFeedHandler::new(
709 self.signal.clone(),
710 cmd_rx,
711 bytes_rx,
712 out_tx.clone(),
713 subscriptions_state.clone(),
714 self.request_id_counter.clone(),
715 );
716
717 cmd_tx
718 .send(BinanceFuturesWsStreamsCommand::SetClient(client))
719 .map_err(|e| BinanceWsError::ClientError(format!("Failed to set client: {e}")))?;
720
721 let signal = self.signal.clone();
722 let token = cancellation_token.clone();
723 let subs = subscriptions_state.clone();
724 let resubscribe_tx = cmd_tx.clone();
725
726 let mut handler_task = TaskSlot::new();
727 if let Err(e) = handler_task.spawn(async move {
728 loop {
729 tokio::select! {
730 () = token.cancelled() => {
731 log::debug!("Handler task cancelled");
732 break;
733 }
734 result = handler.next() => {
735 match result {
736 Some(BinanceFuturesWsStreamsMessage::Reconnected(abandoned)) => {
737 log::info!("WebSocket reconnected, restoring subscriptions");
738 let all_topics = subs.all_topics();
739 for topic in &all_topics {
740 subs.mark_failure(topic);
741 }
742
743 let streams = subs.all_topics();
744 if !streams.is_empty()
745 && let Err(e) = resubscribe_tx.send(BinanceFuturesWsStreamsCommand::Subscribe { streams }) {
746 log::error!("Failed to resubscribe after reconnect: {e}");
747 }
748
749 if out_tx
750 .send(BinanceFuturesWsStreamsMessage::Reconnected(abandoned))
751 .is_err()
752 {
753 log::debug!("Output channel closed");
754 break;
755 }
756 }
757 Some(msg) => {
758 if out_tx.send(msg).is_err() {
759 log::debug!("Output channel closed");
760 break;
761 }
762 }
763 None => {
764 if signal.load(Ordering::Relaxed) {
765 log::debug!("Handler received shutdown signal");
766 } else {
767 log::warn!("Handler loop ended unexpectedly");
768 }
769 break;
770 }
771 }
772 }
773 }
774 }
775 }) {
776 cancellation_token.cancel();
777 bytes_task.abort();
778 let mut shutdown_errors = Vec::new();
779
780 if let Some(error) =
781 finish_slot_task(&mut handler_task, "Binance futures WS handler").await
782 {
783 shutdown_errors.push(error);
784 }
785
786 if let Some(error) =
787 finish_slot_task(&mut bytes_task, "Binance futures WS bytes").await
788 {
789 shutdown_errors.push(error);
790 }
791 return Err(BinanceWsError::ClientError(if shutdown_errors.is_empty() {
792 format!("Failed to start futures WS handler task: {e}")
793 } else {
794 format!(
795 "Failed to start futures WS handler task: {e}; startup rollback failed: {}",
796 shutdown_errors.join("; ")
797 )
798 }));
799 }
800
801 if let Some(control) = &socket_control {
802 control.register(move || reconnect_handle.request_reconnect());
803 }
804
805 Ok(ConnectionSlot {
806 cmd_tx,
807 streams: Vec::new(),
808 subscriptions_state,
809 handler_task,
810 bytes_task,
811 cancellation_token,
812 connection_mode,
813 socket_control,
814 shutdown_errors: Vec::new(),
815 })
816 }
817}
818
819struct ConnectionSlots(Mutex<Vec<ConnectionSlot>>);
820
821impl std::ops::Deref for ConnectionSlots {
822 type Target = Mutex<Vec<ConnectionSlot>>;
823
824 fn deref(&self) -> &Self::Target {
825 &self.0
826 }
827}
828
829impl Drop for ConnectionSlots {
830 fn drop(&mut self) {
831 for slot in self.0.get_mut().iter() {
832 slot.cancellation_token.cancel();
833 if let Some(handle) = slot.handler_task.as_ref() {
834 handle.abort();
835 }
836
837 if let Some(handle) = slot.bytes_task.as_ref() {
838 handle.abort();
839 }
840
841 if let Some(control) = &slot.socket_control {
842 control.deregister();
843 }
844 }
845 }
846}
847
848struct ConnectionSlotBatch<'a> {
849 owner: &'a Mutex<Vec<ConnectionSlot>>,
850 slots: Vec<ConnectionSlot>,
851}
852
853impl<'a> ConnectionSlotBatch<'a> {
854 fn take(owner: &'a Mutex<Vec<ConnectionSlot>>) -> Self {
855 let slots = std::mem::take(&mut *owner.lock());
856 Self { owner, slots }
857 }
858}
859
860impl Drop for ConnectionSlotBatch<'_> {
861 fn drop(&mut self) {
862 self.owner.lock().extend(self.slots.drain(..));
863 }
864}
865
866async fn finish_slot_task(slot: &mut TaskSlot<()>, owner: &str) -> Option<String> {
867 let outcome = finish_task(
868 slot,
869 std::time::Duration::from_secs(2),
870 std::time::Duration::from_secs(2),
871 )
872 .await?;
873
874 match outcome {
875 TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => None,
876 TaskJoinOutcome::Failed(e) => Some(format!("{owner} task failed: {e}")),
877 TaskJoinOutcome::Incomplete => Some(format!("{owner} task did not stop after abort")),
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use rstest::rstest;
884
885 use super::*;
886
887 #[tokio::test]
888 async fn test_cancelled_close_retains_connection_slot() {
889 let mut client = BinanceFuturesWebSocketClient::new(
890 BinanceProductType::UsdM,
891 BinanceEnvironment::Testnet,
892 None,
893 None,
894 None,
895 None,
896 TransportBackend::default(),
897 )
898 .unwrap();
899 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel();
900 client.slots.lock().push(ConnectionSlot {
901 cmd_tx,
902 streams: Vec::new(),
903 subscriptions_state: SubscriptionState::new('@'),
904 handler_task: TaskSlot::from_handle(tokio::spawn(std::future::pending())),
905 bytes_task: TaskSlot::new(),
906 cancellation_token: CancellationToken::new(),
907 connection_mode: Arc::new(AtomicU8::new(ConnectionMode::Active as u8)),
908 socket_control: None,
909 shutdown_errors: Vec::new(),
910 });
911
912 {
913 let close = client.close();
914 tokio::pin!(close);
915 tokio::select! {
916 result = &mut close => panic!("close completed unexpectedly: {result:?}"),
917 command = cmd_rx.recv() => assert!(command.is_some()),
918 }
919 }
920
921 let slots = client.slots.lock();
922 assert_eq!(slots.len(), 1);
923 assert!(slots[0].handler_task.is_some());
924 }
925
926 #[rstest]
927 fn test_with_proxy_preserves_proxy_url() {
928 let client = BinanceFuturesWebSocketClient::new(
929 BinanceProductType::UsdM,
930 BinanceEnvironment::Testnet,
931 None,
932 None,
933 None,
934 None,
935 TransportBackend::default(),
936 )
937 .unwrap()
938 .with_proxy(Some("socks5://proxy.example:1080".to_string()));
939
940 assert_eq!(
941 client.proxy_url.as_ref().map(SecretString::expose_secret),
942 Some("socks5://proxy.example:1080")
943 );
944 }
945}