1use std::{
23 fmt::Debug,
24 sync::{
25 Arc, OnceLock,
26 atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
27 },
28 time::{Duration, Instant},
29};
30
31use bytes::Bytes;
32use nautilus_core::string::secret::{REDACTED, SecretString};
33use nautilus_live::{
34 book::snapshot::SnapshotGate,
35 task::{SharedTaskSlot, TaskJoinOutcome},
36};
37use nautilus_network::{
38 SocketState, SocketStateSink, WriterSender,
39 mode::ReconnectRequestOutcome,
40 socket::{
41 SocketClient, SocketConfig, SocketHeartbeat, SocketReconnectHandle, SocketReconnectReplay,
42 TcpMessageHandler, WriterCommand,
43 },
44};
45use parking_lot::{Mutex, MutexGuard};
46use tokio::sync::watch; use tokio_tungstenite::tungstenite::stream::Mode;
48use zeroize::Zeroizing;
49
50use super::{
51 config::{
52 BETFAIR_STREAM_HEARTBEAT_MAX_MS, BETFAIR_STREAM_HEARTBEAT_MIN_MS, BetfairStreamConfig,
53 },
54 error::BetfairStreamError,
55 messages::{
56 Authentication, CricketSubscription, MarketDataFilter, MarketSubscription, OrderFilter,
57 OrderSubscription, RaceSubscription, Status, StreamMarketFilter, StreamMessage,
58 stream_decode,
59 },
60};
61use crate::common::{
62 consts::{
63 BETFAIR_STREAM_SERVER_HEARTBEAT_MS, STREAM_OP_MARKET_SUBSCRIPTION,
64 STREAM_OP_ORDER_SUBSCRIPTION,
65 },
66 credential::BetfairCredential,
67 enums::{ChangeType, SegmentType, StatusErrorCode},
68};
69
70pub(crate) type StreamMessageHandler = Arc<dyn Fn(StreamMessage) + Send + Sync>;
71
72#[derive(Clone, Copy, Debug)]
73pub(crate) enum HeartbeatTimeoutSource {
74 Outbound,
75 Server,
76}
77
78const AUTH_REQUEST_ID: u64 = 1;
79const STREAM_STATUS_SUCCESS: &str = "SUCCESS";
80const STREAM_DEGRADED_STATUS: i32 = 503;
81const MARKET_SUBSCRIPTION_REPLAY_KEY: u64 = 1;
82const ORDER_SUBSCRIPTION_REPLAY_KEY: u64 = 2;
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85#[repr(u8)]
86pub enum StreamLifecycleState {
87 Disconnected,
88 Idle,
89 Pending,
90 Active,
91 Degraded,
92 Rejected,
93}
94
95impl StreamLifecycleState {
96 fn from_atomic(value: &AtomicU8) -> Self {
97 match value.load(Ordering::Acquire) {
98 1 => Self::Idle,
99 2 => Self::Pending,
100 3 => Self::Active,
101 4 => Self::Degraded,
102 5 => Self::Rejected,
103 _ => Self::Disconnected,
104 }
105 }
106}
107
108#[derive(Debug)]
109struct LifecycleState {
110 value: AtomicU8,
111 changed: watch::Sender<()>,
112}
113
114impl LifecycleState {
115 fn new(state: StreamLifecycleState) -> Self {
116 Self {
117 value: AtomicU8::new(state as u8),
118 changed: watch::channel(()).0,
119 }
120 }
121
122 fn get(&self) -> StreamLifecycleState {
123 StreamLifecycleState::from_atomic(&self.value)
124 }
125
126 fn set(&self, state: StreamLifecycleState) {
127 self.value.store(state as u8, Ordering::Release);
128 self.changed.send_replace(());
129 }
130}
131
132#[derive(Debug)]
133struct ProtocolLifecycle {
134 transport_connected: AtomicBool,
135 authenticated: LifecycleState,
136 market: LifecycleState,
137 market_was_current: AtomicBool,
138 market_requires_image: AtomicBool,
139 market_image_tainted: AtomicBool,
140 order: LifecycleState,
141 order_was_current: AtomicBool,
142 order_requires_image: AtomicBool,
143 order_image_tainted: AtomicBool,
144}
145
146impl Default for ProtocolLifecycle {
147 fn default() -> Self {
148 Self {
149 transport_connected: AtomicBool::new(false),
150 authenticated: LifecycleState::new(StreamLifecycleState::Disconnected),
151 market: LifecycleState::new(StreamLifecycleState::Idle),
152 market_was_current: AtomicBool::new(false),
153 market_requires_image: AtomicBool::new(false),
154 market_image_tainted: AtomicBool::new(false),
155 order: LifecycleState::new(StreamLifecycleState::Idle),
156 order_was_current: AtomicBool::new(false),
157 order_requires_image: AtomicBool::new(false),
158 order_image_tainted: AtomicBool::new(false),
159 }
160 }
161}
162
163impl ProtocolLifecycle {
164 fn on_transport(&self, state: SocketState, market_id: u64, order_id: u64) {
165 let connected = state == SocketState::Connected;
166 self.transport_connected.store(connected, Ordering::Release);
167 self.authenticated.set(if connected {
168 StreamLifecycleState::Pending
169 } else {
170 StreamLifecycleState::Disconnected
171 });
172 self.market
173 .set(subscription_transport_state(connected, market_id));
174 self.market_was_current.store(false, Ordering::Release);
175 self.market_image_tainted.store(false, Ordering::Release);
176 self.order
177 .set(subscription_transport_state(connected, order_id));
178 self.order_was_current.store(false, Ordering::Release);
179 self.order_image_tainted.store(false, Ordering::Release);
180 }
181
182 fn on_status(&self, status: &Status, market_id: u64, order_id: u64) {
183 let Some(id) = status.id else {
184 return;
185 };
186 let next = if status.status_code.as_deref() == Some(STREAM_STATUS_SUCCESS)
187 && status.error_code.is_none()
188 {
189 StreamLifecycleState::Active
190 } else {
191 StreamLifecycleState::Rejected
192 };
193
194 if id == AUTH_REQUEST_ID {
195 self.authenticated.set(next);
196 } else if id == market_id {
197 self.market.set(if next == StreamLifecycleState::Active {
198 StreamLifecycleState::Pending
199 } else {
200 next
201 });
202 } else if id == order_id {
203 self.order.set(if next == StreamLifecycleState::Active {
204 StreamLifecycleState::Pending
205 } else {
206 next
207 });
208 }
209 }
210
211 fn on_change(
212 state: &LifecycleState,
213 was_current: &AtomicBool,
214 requires_image: &AtomicBool,
215 status: Option<i32>,
216 change_type: Option<ChangeType>,
217 segment_type: Option<SegmentType>,
218 ) {
219 let complete = change_complete(segment_type);
220 let initial = change_type == Some(ChangeType::SubImage)
221 || (change_type == Some(ChangeType::ResubDelta)
222 && !requires_image.load(Ordering::Acquire));
223
224 if status == Some(STREAM_DEGRADED_STATUS) {
225 state.set(StreamLifecycleState::Degraded);
226 return;
227 }
228
229 let current = state.get();
230
231 if status.is_none()
232 && complete
233 && (initial
234 || (current == StreamLifecycleState::Degraded
235 && was_current.load(Ordering::Acquire)))
236 {
237 if change_type == Some(ChangeType::SubImage) {
238 requires_image.store(false, Ordering::Release);
239 }
240 was_current.store(true, Ordering::Release);
241 state.set(StreamLifecycleState::Active);
242 }
243 }
244}
245
246const fn subscription_transport_state(connected: bool, id: u64) -> StreamLifecycleState {
247 if !connected {
248 StreamLifecycleState::Disconnected
249 } else if id == 0 {
250 StreamLifecycleState::Idle
251 } else {
252 StreamLifecycleState::Pending
253 }
254}
255
256async fn wait_for_lifecycle_state(state: &LifecycleState, expected: StreamLifecycleState) {
257 let mut changed_rx = state.changed.subscribe();
258 loop {
259 if state.get() == expected {
260 return;
261 }
262 changed_rx
263 .changed()
264 .await
265 .expect("lifecycle sender lives as long as the borrowed client");
266 }
267}
268
269#[derive(Debug)]
279pub struct BetfairStreamClient {
280 socket: SocketClient,
281 market_sub_tx: watch::Sender<Option<MarketSubscription>>,
282 market_clk_tx: watch::Sender<Option<String>>,
283 market_initial_clk_tx: watch::Sender<Option<String>>,
284 order_sub_tx: watch::Sender<Option<OrderSubscription>>,
285 order_clk_tx: watch::Sender<Option<String>>,
286 order_initial_clk_tx: watch::Sender<Option<String>>,
287 market_active_sub_id: Arc<AtomicU64>,
288 order_active_sub_id: Arc<AtomicU64>,
289 request_id: Arc<AtomicU64>,
290 market_state_lock: Arc<Mutex<()>>,
291 order_state_lock: Arc<Mutex<()>>,
292 auth_tx: watch::Sender<StreamAuth>,
293 reconnect_auth: Arc<ReconnectAuthState>,
294 lifecycle: Arc<ProtocolLifecycle>,
295 dead_peer_enabled: Arc<AtomicBool>,
296 dead_peer_timeout_ms: Arc<AtomicU64>,
297 dead_peer_timeout_override: bool,
298 dead_peer_task: SharedTaskSlot<()>,
299 closed: AtomicBool,
300}
301
302impl BetfairStreamClient {
303 pub async fn connect(
309 credential: &BetfairCredential,
310 session_token: impl Into<SecretString>,
311 handler: TcpMessageHandler,
312 config: BetfairStreamConfig,
313 ) -> Result<Self, BetfairStreamError> {
314 Self::connect_inner(
315 credential,
316 session_token.into(),
317 StreamHandler::Raw(handler),
318 config,
319 HeartbeatTimeoutSource::Server,
320 None,
321 )
322 .await
323 }
324
325 pub(crate) async fn connect_with_state_sink(
331 credential: &BetfairCredential,
332 session_token: SecretString,
333 handler: StreamMessageHandler,
334 config: BetfairStreamConfig,
335 heartbeat_timeout_source: HeartbeatTimeoutSource,
336 state_sink: Option<SocketStateSink>,
337 ) -> Result<Self, BetfairStreamError> {
338 Self::connect_inner(
339 credential,
340 session_token,
341 StreamHandler::Decoded(handler),
342 config,
343 heartbeat_timeout_source,
344 state_sink,
345 )
346 .await
347 }
348
349 async fn connect_inner(
350 credential: &BetfairCredential,
351 session_token: SecretString,
352 handler: StreamHandler,
353 config: BetfairStreamConfig,
354 heartbeat_timeout_source: HeartbeatTimeoutSource,
355 state_sink: Option<SocketStateSink>,
356 ) -> Result<Self, BetfairStreamError> {
357 config
358 .validate()
359 .map_err(|e| BetfairStreamError::ProtocolError(e.to_string()))?;
360 let auth = Zeroizing::new(Authentication::with_id(
361 credential.app_key(),
362 session_token,
363 AUTH_REQUEST_ID,
364 ));
365 let auth_bytes = Zeroizing::new(serde_json::to_vec(&*auth)?);
366 let reconnect_auth = Arc::new(ReconnectAuthState::default());
367 let (auth_tx, auth_rx) = watch::channel(StreamAuth {
368 generation: 0,
369 bytes: auth_bytes.clone(),
370 });
371 let mode = if config.use_tls {
372 Mode::Tls
373 } else {
374 Mode::Plain
375 };
376
377 let (market_clk_tx, market_clk_rx) = watch::channel(None::<String>);
378 let (market_initial_clk_tx, market_initial_clk_rx) = watch::channel(None::<String>);
379 let (order_clk_tx, order_clk_rx) = watch::channel(None::<String>);
380 let (order_initial_clk_tx, order_initial_clk_rx) = watch::channel(None::<String>);
381 let (market_sub_tx, market_sub_rx) = watch::channel(None::<MarketSubscription>);
382 let (order_sub_tx, order_sub_rx) = watch::channel(None::<OrderSubscription>);
383
384 let market_sub_tx_h = market_sub_tx.clone();
386 let order_sub_tx_h = order_sub_tx.clone();
387 let (market_clk_tx_h, market_initial_clk_tx_h) =
388 (market_clk_tx.clone(), market_initial_clk_tx.clone());
389 let (order_clk_tx_h, order_initial_clk_tx_h) =
390 (order_clk_tx.clone(), order_initial_clk_tx.clone());
391
392 let market_active_sub_id = Arc::new(AtomicU64::new(0));
393 let order_active_sub_id = Arc::new(AtomicU64::new(0));
394 let request_id = Arc::new(AtomicU64::new(AUTH_REQUEST_ID + 1));
395 let request_id_h = Arc::clone(&request_id);
396 let market_state_lock = Arc::new(Mutex::new(()));
397 let order_state_lock = Arc::new(Mutex::new(()));
398 let market_state_lock_h = Arc::clone(&market_state_lock);
399 let order_state_lock_h = Arc::clone(&order_state_lock);
400 let writer_tx_h = Arc::new(OnceLock::new());
401 let writer_tx_handler = Arc::clone(&writer_tx_h);
402 let market_active_sub_id_h = Arc::clone(&market_active_sub_id);
403 let order_active_sub_id_h = Arc::clone(&order_active_sub_id);
404 let reconnect_auth_h = Arc::clone(&reconnect_auth);
405 let lifecycle = Arc::new(ProtocolLifecycle::default());
406 let lifecycle_h = Arc::clone(&lifecycle);
407 let last_inbound = Arc::new(Mutex::new(Instant::now()));
408 let last_inbound_h = Arc::clone(&last_inbound);
409 let dead_peer_enabled = Arc::new(AtomicBool::new(false));
410 let dead_peer_timeout_ms = Arc::new(AtomicU64::new(
411 config.dead_peer_timeout_secs().saturating_mul(1_000),
412 ));
413 let dead_peer_timeout_ms_h = Arc::clone(&dead_peer_timeout_ms);
414 let timeout_override = config.heartbeat_timeout_secs.is_some();
415
416 let message_handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
417 *last_inbound_h.lock() = Instant::now();
418 let Some(msg) = handler.decode(data) else {
419 return;
420 };
421
422 match &msg {
423 StreamMessage::MarketChange(mcm) => {
424 let _state = lock_stream_state(&market_state_lock_h);
425 let active = market_active_sub_id_h.load(Ordering::SeqCst);
426 let current = active == 0 || mcm.id.is_none_or(|id| id == active);
427 if !current {
428 return;
429 }
430
431 if mcm.status == Some(STREAM_DEGRADED_STATUS) {
432 if mcm.segment_type.is_some() {
433 lifecycle_h
434 .market_image_tainted
435 .store(true, Ordering::Release);
436 }
437 ProtocolLifecycle::on_change(
438 &lifecycle_h.market,
439 &lifecycle_h.market_was_current,
440 &lifecycle_h.market_requires_image,
441 mcm.status,
442 mcm.ct,
443 mcm.segment_type,
444 );
445 return;
446 }
447
448 let image_start = mcm.ct == Some(ChangeType::SubImage)
449 && matches!(mcm.segment_type, None | Some(SegmentType::SegStart));
450 let complete = change_complete(mcm.segment_type);
451 if image_start && mcm.status.is_none() {
452 lifecycle_h
453 .market_image_tainted
454 .store(false, Ordering::Release);
455 } else if lifecycle_h.market_image_tainted.load(Ordering::Acquire) {
456 if complete
457 && let Err(e) = reissue_market_subscription(
458 &request_id_h,
459 &market_active_sub_id_h,
460 &lifecycle_h,
461 &market_sub_tx_h,
462 &market_clk_tx_h,
463 &market_initial_clk_tx_h,
464 writer_tx_handler.get(),
465 )
466 {
467 log::error!("Failed to recover Betfair market stream: {e}");
468 }
469 return;
470 }
471
472 let lifecycle_state = lifecycle_h.market.get();
473 if lifecycle_state == StreamLifecycleState::Degraded
474 && mcm.ct != Some(ChangeType::SubImage)
475 {
476 if complete
477 && let Err(e) = reissue_market_subscription(
478 &request_id_h,
479 &market_active_sub_id_h,
480 &lifecycle_h,
481 &market_sub_tx_h,
482 &market_clk_tx_h,
483 &market_initial_clk_tx_h,
484 writer_tx_handler.get(),
485 )
486 {
487 log::error!("Failed to recover Betfair market stream: {e}");
488 }
489 return;
490 }
491
492 if lifecycle_h.market_requires_image.load(Ordering::Acquire)
493 && mcm.ct == Some(ChangeType::ResubDelta)
494 {
495 return;
496 }
497
498 ProtocolLifecycle::on_change(
499 &lifecycle_h.market,
500 &lifecycle_h.market_was_current,
501 &lifecycle_h.market_requires_image,
502 mcm.status,
503 mcm.ct,
504 mcm.segment_type,
505 );
506 update_stream_state(
507 &mcm.clk,
508 &mcm.initial_clk,
509 mcm.heartbeat_ms,
510 &market_clk_tx_h,
511 &market_initial_clk_tx_h,
512 timeout_override,
513 &dead_peer_timeout_ms_h,
514 );
515 handler.handle(data, msg);
516 }
517 StreamMessage::OrderChange(ocm) => {
518 let _state = lock_stream_state(&order_state_lock_h);
519 let active = order_active_sub_id_h.load(Ordering::SeqCst);
520 let current = active == 0 || ocm.id.is_none_or(|id| id == active);
521 if !current {
522 return;
523 }
524
525 if ocm.status == Some(STREAM_DEGRADED_STATUS) {
526 if ocm.segment_type.is_some() {
527 lifecycle_h
528 .order_image_tainted
529 .store(true, Ordering::Release);
530 }
531 ProtocolLifecycle::on_change(
532 &lifecycle_h.order,
533 &lifecycle_h.order_was_current,
534 &lifecycle_h.order_requires_image,
535 ocm.status,
536 ocm.ct,
537 ocm.segment_type,
538 );
539 handler.handle(data, msg);
540 return;
541 }
542
543 let image_start = ocm.ct == Some(ChangeType::SubImage)
544 && matches!(ocm.segment_type, None | Some(SegmentType::SegStart));
545 let complete = change_complete(ocm.segment_type);
546 if image_start && ocm.status.is_none() {
547 lifecycle_h
548 .order_image_tainted
549 .store(false, Ordering::Release);
550 } else if lifecycle_h.order_image_tainted.load(Ordering::Acquire) {
551 if complete {
552 reissue_order_subscription(
553 &request_id_h,
554 &order_active_sub_id_h,
555 &lifecycle_h,
556 &order_sub_tx_h,
557 &order_clk_tx_h,
558 &order_initial_clk_tx_h,
559 writer_tx_handler.get(),
560 );
561 }
562 return;
563 }
564
565 let lifecycle_state = lifecycle_h.order.get();
566 if lifecycle_state == StreamLifecycleState::Degraded
567 && lifecycle_h.order_requires_image.load(Ordering::Acquire)
568 && ocm.ct != Some(ChangeType::SubImage)
569 {
570 if complete {
571 reissue_order_subscription(
572 &request_id_h,
573 &order_active_sub_id_h,
574 &lifecycle_h,
575 &order_sub_tx_h,
576 &order_clk_tx_h,
577 &order_initial_clk_tx_h,
578 writer_tx_handler.get(),
579 );
580 }
581 return;
582 }
583
584 if lifecycle_h.order_requires_image.load(Ordering::Acquire)
585 && ocm.ct == Some(ChangeType::ResubDelta)
586 {
587 return;
588 }
589
590 ProtocolLifecycle::on_change(
591 &lifecycle_h.order,
592 &lifecycle_h.order_was_current,
593 &lifecycle_h.order_requires_image,
594 ocm.status,
595 ocm.ct,
596 ocm.segment_type,
597 );
598 update_stream_state(
599 &ocm.clk,
600 &ocm.initial_clk,
601 ocm.heartbeat_ms,
602 &order_clk_tx_h,
603 &order_initial_clk_tx_h,
604 timeout_override,
605 &dead_peer_timeout_ms_h,
606 );
607 handler.handle(data, msg);
608 }
609 StreamMessage::Status(status) => {
610 let _market_state = lock_stream_state(&market_state_lock_h);
611 let _order_state = lock_stream_state(&order_state_lock_h);
612 let market_id = market_active_sub_id_h.load(Ordering::Acquire);
613 let order_id = order_active_sub_id_h.load(Ordering::Acquire);
614 lifecycle_h.on_status(status, market_id, order_id);
615 if status.error_code == Some(StatusErrorCode::InvalidClock) {
617 if market_id > 0 && status.id == Some(market_id) {
618 let _ = market_clk_tx_h.send(None);
619 let _ = market_initial_clk_tx_h.send(None);
620 lifecycle_h
621 .market_requires_image
622 .store(true, Ordering::Release);
623 lifecycle_h
624 .market_image_tainted
625 .store(false, Ordering::Release);
626 log::warn!(
627 "Betfair market stream INVALID_CLOCK: clocks cleared, \
628 next reconnect will request a full image",
629 );
630 } else if order_id > 0 && status.id == Some(order_id) {
631 let _ = order_clk_tx_h.send(None);
632 let _ = order_initial_clk_tx_h.send(None);
633 lifecycle_h
634 .order_requires_image
635 .store(true, Ordering::Release);
636 lifecycle_h
637 .order_image_tainted
638 .store(false, Ordering::Release);
639 log::warn!(
640 "Betfair order stream INVALID_CLOCK: clocks cleared, \
641 next reconnect will request a full image",
642 );
643 }
644 } else if status.connection_closed {
645 log::warn!(
646 "Betfair stream connection closed by server: {:?} - {:?}",
647 status.error_code,
648 status.error_message,
649 );
650 } else if status.error_code.is_some() {
651 log::warn!(
652 "Betfair stream status error: {:?} - {:?}",
653 status.error_code,
654 status.error_message,
655 );
656 }
657 handler.handle(data, msg);
658 }
659 StreamMessage::Connection(_) => {
660 reconnect_auth_h.request_pending();
661 handler.handle(data, msg);
662 }
663 _ => {
664 handler.handle(data, msg);
665 }
666 }
667 });
668
669 let auth_reconnect = auth_rx;
670 let reconnect_auth_replay = Arc::clone(&reconnect_auth);
671 let market_state_replay = Arc::clone(&market_state_lock);
672 let order_state_replay = Arc::clone(&order_state_lock);
673 let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
674 let mut replay = Vec::with_capacity(3);
675 let auth = auth_reconnect.borrow().clone();
676 reconnect_auth_replay.record_replay(auth.generation);
677
678 replay.push(Bytes::copy_from_slice(&auth.bytes));
679
680 {
681 let _state = lock_stream_state(&market_state_replay);
682
683 if let Some(mut sub) = market_sub_rx.borrow().clone() {
684 sub.clk = market_clk_rx.borrow().clone();
685 sub.initial_clk = market_initial_clk_rx.borrow().clone();
686 if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
687 replay.push(Bytes::from(sub_bytes));
688 }
689 }
690 }
691
692 {
693 let _state = lock_stream_state(&order_state_replay);
694
695 if let Some(mut sub) = order_sub_rx.borrow().clone() {
696 sub.clk = order_clk_rx.borrow().clone();
697 sub.initial_clk = order_initial_clk_rx.borrow().clone();
698 if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
699 replay.push(Bytes::from(sub_bytes));
700 }
701 }
702 }
703
704 replay
705 });
706
707 let url = format!("{}:{}", config.host, config.port);
708 let lifecycle_sink = Arc::clone(&lifecycle);
709 let market_id_sink = Arc::clone(&market_active_sub_id);
710 let order_id_sink = Arc::clone(&order_active_sub_id);
711 let last_inbound_sink = Arc::clone(&last_inbound);
712 let market_state_sink = Arc::clone(&market_state_lock);
713 let order_state_sink = Arc::clone(&order_state_lock);
714 let lifecycle_callback = move |state| {
715 let _market_state = lock_stream_state(&market_state_sink);
716 let _order_state = lock_stream_state(&order_state_sink);
717 lifecycle_sink.on_transport(
718 state,
719 market_id_sink.load(Ordering::Acquire),
720 order_id_sink.load(Ordering::Acquire),
721 );
722 *last_inbound_sink.lock() = Instant::now();
723 };
724 let state_sink = match state_sink {
725 Some(sink) => sink.with_callback(lifecycle_callback),
726 None => SocketStateSink::new(lifecycle_callback),
727 };
728 let socket_config = SocketConfig {
729 url,
730 mode,
731 suffix: b"\r\n".to_vec(),
732 message_handler: Some(message_handler),
733 heartbeat: outbound_heartbeat(config.heartbeat_secs),
734 connect_timeout_ms: None,
735 reconnect_delay_initial_ms: Some(config.reconnect_delay_initial_ms),
736 reconnect_delay_max_ms: Some(config.reconnect_delay_max_ms),
737 reconnect_backoff_factor: None,
738 reconnect_jitter_ms: None,
739 connection_max_retries: None,
740 reconnect_max_attempts: None,
741 heartbeat_timeout_secs: heartbeat_timeout(
742 heartbeat_timeout_source,
743 config.heartbeat_secs,
744 config.heartbeat_timeout_secs,
745 ),
746 writer_capacity: None,
747 certs_dir: None,
748 };
749
750 let socket = SocketClient::builder()
751 .config(socket_config)
752 .state_sink(state_sink)
753 .reconnect_replay(reconnect_replay)
754 .connect()
755 .await
756 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
757 writer_tx_h
758 .set(socket.writer_tx.clone())
759 .expect("Betfair stream writer must only be initialized once");
760 reconnect_auth.set_handle(socket.reconnect_handle());
761
762 socket
763 .send_bytes(auth_bytes.as_slice().to_vec())
764 .await
765 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
766
767 let dead_peer_task = SharedTaskSlot::new();
768
769 if matches!(heartbeat_timeout_source, HeartbeatTimeoutSource::Server) {
770 let reconnect = socket.reconnect_handle();
771 let enabled = Arc::clone(&dead_peer_enabled);
772 let last = Arc::clone(&last_inbound);
773 let timeout_ms = Arc::clone(&dead_peer_timeout_ms);
774
775 dead_peer_task
776 .spawn(async move {
777 loop {
778 tokio::time::sleep(Duration::from_millis(100)).await;
779
780 if !enabled.load(Ordering::Acquire) {
781 continue;
782 }
783 let timeout = Duration::from_millis(timeout_ms.load(Ordering::Acquire));
784 if last.lock().elapsed() >= timeout {
785 let _ = reconnect.request_reconnect();
786 }
787 }
788 })
789 .map_err(|e| {
790 BetfairStreamError::ConnectionFailed(format!(
791 "failed to start dead-peer monitor: {e}"
792 ))
793 })?;
794 }
795
796 Ok(Self {
797 socket,
798 market_sub_tx,
799 market_clk_tx,
800 market_initial_clk_tx,
801 order_sub_tx,
802 order_clk_tx,
803 order_initial_clk_tx,
804 market_active_sub_id,
805 order_active_sub_id,
806 request_id,
807 market_state_lock,
808 order_state_lock,
809 auth_tx,
810 reconnect_auth,
811 lifecycle,
812 dead_peer_enabled,
813 dead_peer_timeout_ms,
814 dead_peer_timeout_override: timeout_override,
815 dead_peer_task,
816 closed: AtomicBool::new(false),
817 })
818 }
819
820 pub async fn subscribe_markets(
828 &self,
829 market_filter: StreamMarketFilter,
830 data_filter: MarketDataFilter,
831 heartbeat_ms: Option<u64>,
832 conflate_ms: Option<u64>,
833 ) -> Result<(), BetfairStreamError> {
834 self.write_market_subscription(
835 market_filter,
836 data_filter,
837 heartbeat_ms,
838 conflate_ms,
839 &SnapshotGate::default(),
840 )
841 }
842
843 pub(crate) fn write_market_subscription(
853 &self,
854 market_filter: StreamMarketFilter,
855 data_filter: MarketDataFilter,
856 heartbeat_ms: Option<u64>,
857 conflate_ms: Option<u64>,
858 gate: &SnapshotGate,
859 ) -> Result<(), BetfairStreamError> {
860 if self.closed.load(Ordering::SeqCst) || self.socket.is_closed() {
861 return Err(BetfairStreamError::Disconnected(
862 "stream client is closed".to_string(),
863 ));
864 }
865 let heartbeat_ms = heartbeat_ms.unwrap_or(BETFAIR_STREAM_SERVER_HEARTBEAT_MS);
866 validate_subscription_heartbeat(heartbeat_ms)?;
867 self.update_dead_peer_timeout(heartbeat_ms);
868 let _state = lock_stream_state(&self.market_state_lock);
869 let id = self.request_id.fetch_add(1, Ordering::Relaxed);
870 self.market_active_sub_id.store(id, Ordering::SeqCst);
873 self.lifecycle.market.set(StreamLifecycleState::Pending);
874 self.lifecycle
875 .market_was_current
876 .store(false, Ordering::Release);
877 self.lifecycle
878 .market_requires_image
879 .store(true, Ordering::Release);
880 self.lifecycle
881 .market_image_tainted
882 .store(false, Ordering::Release);
883 self.dead_peer_enabled.store(true, Ordering::Release);
884 let sub = MarketSubscription {
885 op: STREAM_OP_MARKET_SUBSCRIPTION.to_string(),
886 id: Some(id),
887 market_filter,
888 market_data_filter: data_filter,
889 clk: None,
890 conflate_ms,
891 heartbeat_ms: Some(heartbeat_ms),
892 initial_clk: None,
893 segmentation_enabled: Some(true),
894 };
895
896 let _ = self.market_clk_tx.send(None);
899 let _ = self.market_initial_clk_tx.send(None);
900 let _ = self.market_sub_tx.send(Some(sub.clone()));
901
902 let data = Bytes::from(serde_json::to_vec(&sub)?);
903 self.socket
904 .writer_tx
905 .send(WriterCommand::SendOrReplay {
906 key: MARKET_SUBSCRIPTION_REPLAY_KEY,
907 data,
908 })
909 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
910
911 gate.open();
912 Ok(())
913 }
914
915 pub async fn subscribe_orders(
923 &self,
924 order_filter: Option<OrderFilter>,
925 heartbeat_ms: Option<u64>,
926 ) -> Result<(), BetfairStreamError> {
927 if self.closed.load(Ordering::SeqCst) || self.socket.is_closed() {
928 return Err(BetfairStreamError::Disconnected(
929 "stream client is closed".to_string(),
930 ));
931 }
932 let heartbeat_ms = heartbeat_ms.unwrap_or(BETFAIR_STREAM_SERVER_HEARTBEAT_MS);
933 validate_subscription_heartbeat(heartbeat_ms)?;
934 self.update_dead_peer_timeout(heartbeat_ms);
935 let _state = lock_stream_state(&self.order_state_lock);
936 let id = self.request_id.fetch_add(1, Ordering::Relaxed);
937 self.order_active_sub_id.store(id, Ordering::SeqCst);
938 self.lifecycle.order.set(StreamLifecycleState::Pending);
939 self.lifecycle
940 .order_was_current
941 .store(false, Ordering::Release);
942 self.lifecycle
943 .order_requires_image
944 .store(true, Ordering::Release);
945 self.lifecycle
946 .order_image_tainted
947 .store(false, Ordering::Release);
948 self.dead_peer_enabled.store(true, Ordering::Release);
949 let sub = OrderSubscription {
950 op: STREAM_OP_ORDER_SUBSCRIPTION.to_string(),
951 id: Some(id),
952 order_filter,
953 clk: None,
954 conflate_ms: None,
955 heartbeat_ms: Some(heartbeat_ms),
956 initial_clk: None,
957 segmentation_enabled: Some(true),
958 };
959
960 let _ = self.order_clk_tx.send(None);
963 let _ = self.order_initial_clk_tx.send(None);
964 let _ = self.order_sub_tx.send(Some(sub.clone()));
965
966 let data = Bytes::from(serde_json::to_vec(&sub)?);
967 self.socket
968 .writer_tx
969 .send(WriterCommand::SendOrReplay {
970 key: ORDER_SUBSCRIPTION_REPLAY_KEY,
971 data,
972 })
973 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
974 Ok(())
975 }
976
977 pub(crate) fn request_market_image(
989 &self,
990 gate: &SnapshotGate,
991 ) -> Result<(), BetfairStreamError> {
992 if self.closed.load(Ordering::SeqCst) || self.socket.is_closed() {
993 return Err(BetfairStreamError::Disconnected(
994 "stream client is closed".to_string(),
995 ));
996 }
997
998 let _state = lock_stream_state(&self.market_state_lock);
999 reissue_market_subscription(
1000 &self.request_id,
1001 &self.market_active_sub_id,
1002 &self.lifecycle,
1003 &self.market_sub_tx,
1004 &self.market_clk_tx,
1005 &self.market_initial_clk_tx,
1006 Some(&self.socket.writer_tx),
1007 )?;
1008 gate.open();
1009 Ok(())
1010 }
1011
1012 fn update_dead_peer_timeout(&self, heartbeat_ms: u64) {
1013 if !self.dead_peer_timeout_override {
1014 self.dead_peer_timeout_ms
1015 .store(heartbeat_ms.saturating_mul(2), Ordering::Release);
1016 }
1017 }
1018
1019 #[must_use]
1021 pub fn is_active(&self) -> bool {
1022 self.socket.is_active()
1023 }
1024
1025 #[must_use]
1026 pub fn authentication_state(&self) -> StreamLifecycleState {
1027 self.lifecycle.authenticated.get()
1028 }
1029
1030 #[must_use]
1031 pub fn market_subscription_state(&self) -> StreamLifecycleState {
1032 self.lifecycle.market.get()
1033 }
1034
1035 #[must_use]
1036 pub fn order_subscription_state(&self) -> StreamLifecycleState {
1037 self.lifecycle.order.get()
1038 }
1039
1040 pub async fn wait_for_authentication_state(&self, expected: StreamLifecycleState) {
1048 wait_for_lifecycle_state(&self.lifecycle.authenticated, expected).await;
1049 }
1050
1051 pub async fn wait_for_market_subscription_state(&self, expected: StreamLifecycleState) {
1059 wait_for_lifecycle_state(&self.lifecycle.market, expected).await;
1060 }
1061
1062 pub async fn wait_for_order_subscription_state(&self, expected: StreamLifecycleState) {
1070 wait_for_lifecycle_state(&self.lifecycle.order, expected).await;
1071 }
1072
1073 #[must_use]
1074 pub fn is_authenticated(&self) -> bool {
1075 self.socket.is_active() && self.authentication_state() == StreamLifecycleState::Active
1076 }
1077
1078 #[must_use]
1079 pub fn is_market_ready(&self) -> bool {
1080 self.is_authenticated() && self.market_subscription_state() == StreamLifecycleState::Active
1081 }
1082
1083 #[must_use]
1084 pub fn is_order_ready(&self) -> bool {
1085 self.is_authenticated() && self.order_subscription_state() == StreamLifecycleState::Active
1086 }
1087
1088 pub fn update_auth(&self, app_key: &str, session_token: impl Into<SecretString>) {
1091 update_auth_state(
1092 &self.auth_tx,
1093 &Authentication::with_id(app_key, session_token, AUTH_REQUEST_ID),
1094 );
1095 }
1096
1097 #[must_use]
1102 pub fn request_reconnect(&self) -> bool {
1103 self.request_reconnect_outcome() == ReconnectRequestOutcome::Accepted
1104 }
1105
1106 pub(crate) fn request_reconnect_outcome(&self) -> ReconnectRequestOutcome {
1107 if self.closed.load(Ordering::SeqCst) {
1108 return ReconnectRequestOutcome::Closed;
1109 }
1110 self.reconnect_auth
1111 .request(self.auth_tx.borrow().generation)
1112 }
1113
1114 pub(crate) fn begin_shutdown(&self) {
1115 self.closed.store(true, Ordering::SeqCst);
1116 self.dead_peer_enabled.store(false, Ordering::Release);
1117 self.dead_peer_task.abort();
1118 self.socket.begin_shutdown();
1119 }
1120
1121 pub async fn close(&self) -> Result<(), BetfairStreamError> {
1127 self.begin_shutdown();
1128 self.socket.close().await;
1129
1130 if let Some(outcome) = self
1131 .dead_peer_task
1132 .finish(Duration::from_secs(1), Duration::from_secs(2))
1133 .await
1134 {
1135 match outcome {
1136 TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => Ok(()),
1137 TaskJoinOutcome::Failed(e) => Err(BetfairStreamError::Disconnected(format!(
1138 "dead-peer task failed: {e}"
1139 ))),
1140 TaskJoinOutcome::Incomplete => Err(BetfairStreamError::Timeout(
1141 "dead-peer task did not stop after abort".to_string(),
1142 )),
1143 }
1144 } else {
1145 Ok(())
1146 }
1147 }
1148}
1149
1150impl Drop for BetfairStreamClient {
1151 fn drop(&mut self) {
1152 self.dead_peer_enabled.store(false, Ordering::Release);
1153
1154 self.dead_peer_task.abort();
1155 }
1156}
1157
1158fn lock_stream_state(lock: &Mutex<()>) -> MutexGuard<'_, ()> {
1159 lock.lock()
1160}
1161
1162#[derive(Debug)]
1168pub struct BetfairRaceStreamClient {
1169 socket: SocketClient,
1170 auth_tx: watch::Sender<StreamAuth>,
1171 reconnect_auth: Arc<ReconnectAuthState>,
1172 closed: AtomicBool,
1173}
1174
1175impl BetfairRaceStreamClient {
1176 pub async fn connect(
1186 credential: &BetfairCredential,
1187 session_token: impl Into<SecretString>,
1188 handler: TcpMessageHandler,
1189 config: BetfairStreamConfig,
1190 race_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1191 ) -> Result<Self, BetfairStreamError> {
1192 let subscription = AuxiliaryStreamSubscription::race(race_fatal_tx)?;
1193 Self::connect_with_subscription(
1194 credential,
1195 session_token.into(),
1196 StreamHandler::Raw(handler),
1197 config,
1198 subscription,
1199 None,
1200 )
1201 .await
1202 }
1203
1204 pub(crate) async fn connect_decoded(
1205 credential: &BetfairCredential,
1206 session_token: SecretString,
1207 handler: StreamMessageHandler,
1208 config: BetfairStreamConfig,
1209 race_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1210 state_sink: Option<SocketStateSink>,
1211 ) -> Result<Self, BetfairStreamError> {
1212 let subscription = AuxiliaryStreamSubscription::race(race_fatal_tx)?;
1213 Self::connect_with_subscription(
1214 credential,
1215 session_token,
1216 StreamHandler::Decoded(handler),
1217 config,
1218 subscription,
1219 state_sink,
1220 )
1221 .await
1222 }
1223
1224 pub async fn connect_cricket(
1233 credential: &BetfairCredential,
1234 session_token: impl Into<SecretString>,
1235 handler: TcpMessageHandler,
1236 config: BetfairStreamConfig,
1237 cricket_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1238 ) -> Result<Self, BetfairStreamError> {
1239 let subscription = AuxiliaryStreamSubscription::cricket(cricket_fatal_tx)?;
1240 Self::connect_with_subscription(
1241 credential,
1242 session_token.into(),
1243 StreamHandler::Raw(handler),
1244 config,
1245 subscription,
1246 None,
1247 )
1248 .await
1249 }
1250
1251 pub(crate) async fn connect_cricket_decoded(
1252 credential: &BetfairCredential,
1253 session_token: SecretString,
1254 handler: StreamMessageHandler,
1255 config: BetfairStreamConfig,
1256 cricket_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1257 state_sink: Option<SocketStateSink>,
1258 ) -> Result<Self, BetfairStreamError> {
1259 let subscription = AuxiliaryStreamSubscription::cricket(cricket_fatal_tx)?;
1260 Self::connect_with_subscription(
1261 credential,
1262 session_token,
1263 StreamHandler::Decoded(handler),
1264 config,
1265 subscription,
1266 state_sink,
1267 )
1268 .await
1269 }
1270
1271 async fn connect_with_subscription(
1272 credential: &BetfairCredential,
1273 session_token: SecretString,
1274 handler: StreamHandler,
1275 config: BetfairStreamConfig,
1276 subscription: AuxiliaryStreamSubscription,
1277 state_sink: Option<SocketStateSink>,
1278 ) -> Result<Self, BetfairStreamError> {
1279 let AuxiliaryStreamSubscription {
1280 bytes: sub_bytes,
1281 label,
1282 fatal_hint,
1283 fatal_tx,
1284 } = subscription;
1285
1286 let auth = Zeroizing::new(Authentication::new(credential.app_key(), session_token));
1287 let auth_bytes = Zeroizing::new(serde_json::to_vec(&*auth)?);
1288 let reconnect_auth = Arc::new(ReconnectAuthState::default());
1289 let (auth_tx, auth_rx) = watch::channel(StreamAuth {
1290 generation: 0,
1291 bytes: auth_bytes.clone(),
1292 });
1293
1294 let mode = if config.use_tls {
1295 Mode::Tls
1296 } else {
1297 Mode::Plain
1298 };
1299
1300 let reconnect_auth_h = Arc::clone(&reconnect_auth);
1301 let message_handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1302 let Some(msg) = handler.decode(data) else {
1303 return;
1304 };
1305
1306 if let StreamMessage::Status(status) = &msg {
1307 if let Some(ref code) = status.error_code
1308 && code.is_race_stream_fatal()
1309 {
1310 log::error!(
1311 "Betfair {label} stream fatal error: {:?} - {:?} ({fatal_hint})",
1312 status.error_code,
1313 status.error_message,
1314 );
1315 let _ = fatal_tx.send(());
1316 return;
1317 }
1318
1319 if status.connection_closed {
1320 log::warn!(
1321 "Betfair {label} stream closed: {:?} - {:?}",
1322 status.error_code,
1323 status.error_message,
1324 );
1325 } else if status.error_code.is_some() {
1326 log::warn!(
1327 "Betfair {label} stream status: {:?} - {:?}",
1328 status.error_code,
1329 status.error_message,
1330 );
1331 }
1332 }
1333
1334 if matches!(msg, StreamMessage::Connection(_)) {
1335 reconnect_auth_h.request_pending();
1336 }
1337
1338 handler.handle(data, msg);
1339 });
1340
1341 let auth_reconnect = auth_rx;
1342 let reconnect_auth_replay = Arc::clone(&reconnect_auth);
1343 let sub_reconnect = sub_bytes.clone();
1344 let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
1345 let auth = auth_reconnect.borrow().clone();
1346 reconnect_auth_replay.record_replay(auth.generation);
1347 let mut combined = Zeroizing::new(Vec::with_capacity(
1348 auth.bytes.len() + 2 + sub_reconnect.len(),
1349 ));
1350 combined.extend_from_slice(&auth.bytes);
1351 combined.extend_from_slice(b"\r\n");
1352 combined.extend_from_slice(&sub_reconnect);
1353 vec![Bytes::copy_from_slice(&combined)]
1354 });
1355
1356 let url = format!("{}:{}", config.host, config.port);
1357 let socket_config = SocketConfig {
1358 url,
1359 mode,
1360 suffix: b"\r\n".to_vec(),
1361 message_handler: Some(message_handler),
1362 heartbeat: outbound_heartbeat(config.heartbeat_secs),
1363 connect_timeout_ms: None,
1364 reconnect_delay_initial_ms: Some(config.reconnect_delay_initial_ms),
1365 reconnect_delay_max_ms: Some(config.reconnect_delay_max_ms),
1366 reconnect_backoff_factor: None,
1367 reconnect_jitter_ms: None,
1368 connection_max_retries: None,
1369 reconnect_max_attempts: None,
1370 heartbeat_timeout_secs: heartbeat_timeout(
1371 HeartbeatTimeoutSource::Outbound,
1372 config.heartbeat_secs,
1373 config.heartbeat_timeout_secs,
1374 ),
1375 writer_capacity: None,
1376 certs_dir: None,
1377 };
1378
1379 let socket = SocketClient::builder()
1380 .config(socket_config)
1381 .maybe_state_sink(state_sink)
1382 .reconnect_replay(reconnect_replay)
1383 .connect()
1384 .await
1385 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
1386 reconnect_auth.set_handle(socket.reconnect_handle());
1387
1388 let mut combined =
1389 Zeroizing::new(Vec::with_capacity(auth_bytes.len() + 2 + sub_bytes.len()));
1390 combined.extend_from_slice(&auth_bytes);
1391 combined.extend_from_slice(b"\r\n");
1392 combined.extend_from_slice(&sub_bytes);
1393 socket
1394 .send_bytes(combined.as_slice().to_vec())
1395 .await
1396 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
1397
1398 Ok(Self {
1399 socket,
1400 auth_tx,
1401 reconnect_auth,
1402 closed: AtomicBool::new(false),
1403 })
1404 }
1405
1406 #[must_use]
1408 pub fn is_active(&self) -> bool {
1409 self.socket.is_active()
1410 }
1411
1412 pub fn update_auth(&self, app_key: &str, session_token: impl Into<SecretString>) {
1415 update_auth_state(&self.auth_tx, &Authentication::new(app_key, session_token));
1416 }
1417
1418 #[must_use]
1423 pub fn request_reconnect(&self) -> bool {
1424 self.request_reconnect_outcome() == ReconnectRequestOutcome::Accepted
1425 }
1426
1427 pub(crate) fn request_reconnect_outcome(&self) -> ReconnectRequestOutcome {
1429 if self.closed.load(Ordering::SeqCst) {
1430 return ReconnectRequestOutcome::Closed;
1431 }
1432 self.reconnect_auth
1433 .request(self.auth_tx.borrow().generation)
1434 }
1435
1436 pub(crate) fn begin_shutdown(&self) {
1437 self.closed.store(true, Ordering::SeqCst);
1438 self.socket.begin_shutdown();
1439 }
1440
1441 pub async fn close(&self) {
1443 self.begin_shutdown();
1444 self.socket.close().await;
1445 }
1446}
1447
1448fn update_auth_state(auth_tx: &watch::Sender<StreamAuth>, auth: &Authentication) {
1449 let Ok(bytes) = serde_json::to_vec(auth) else {
1450 return;
1451 };
1452 let bytes = Zeroizing::new(bytes);
1453 auth_tx.send_if_modified(|current| {
1454 if current.bytes == bytes {
1455 return false;
1456 }
1457 *current = StreamAuth {
1458 generation: current.generation.wrapping_add(1),
1459 bytes,
1460 };
1461 true
1462 });
1463}
1464
1465enum StreamHandler {
1466 Raw(TcpMessageHandler),
1467 Decoded(StreamMessageHandler),
1468}
1469
1470impl StreamHandler {
1471 fn decode(&self, data: &[u8]) -> Option<StreamMessage> {
1472 match stream_decode(data) {
1473 Ok(message) => Some(message),
1474 Err(e) => {
1475 match self {
1476 Self::Raw(handler) => handler(data),
1477 Self::Decoded(_) => log::warn!("Failed to decode stream message: {e}"),
1478 }
1479 None
1480 }
1481 }
1482 }
1483
1484 fn handle(&self, data: &[u8], message: StreamMessage) {
1485 match self {
1486 Self::Raw(handler) => handler(data),
1487 Self::Decoded(handler) => handler(message),
1488 }
1489 }
1490}
1491
1492const fn change_complete(segment_type: Option<SegmentType>) -> bool {
1493 matches!(segment_type, None | Some(SegmentType::SegEnd))
1494}
1495
1496fn reissue_market_subscription(
1497 request_id: &AtomicU64,
1498 active_id: &AtomicU64,
1499 lifecycle: &ProtocolLifecycle,
1500 sub_tx: &watch::Sender<Option<MarketSubscription>>,
1501 clk_tx: &watch::Sender<Option<String>>,
1502 initial_clk_tx: &watch::Sender<Option<String>>,
1503 writer_tx: Option<&WriterSender<WriterCommand>>,
1504) -> Result<(), BetfairStreamError> {
1505 let Some(writer_tx) = writer_tx else {
1506 return Err(BetfairStreamError::Disconnected(
1507 "writer is not initialized".to_string(),
1508 ));
1509 };
1510 let Some(mut sub) = sub_tx.borrow().clone() else {
1511 return Err(BetfairStreamError::ProtocolError(
1512 "no retained market subscription".to_string(),
1513 ));
1514 };
1515 let id = request_id.fetch_add(1, Ordering::Relaxed);
1516 sub.id = Some(id);
1517 sub.clk = None;
1518 sub.initial_clk = None;
1519 let data = Bytes::from(serde_json::to_vec(&sub)?);
1520
1521 active_id.store(id, Ordering::SeqCst);
1522 lifecycle.market.set(StreamLifecycleState::Pending);
1523 lifecycle.market_was_current.store(false, Ordering::Release);
1524 lifecycle
1525 .market_requires_image
1526 .store(true, Ordering::Release);
1527 lifecycle
1528 .market_image_tainted
1529 .store(false, Ordering::Release);
1530 let _ = clk_tx.send(None);
1531 let _ = initial_clk_tx.send(None);
1532 let _ = sub_tx.send(Some(sub));
1533
1534 writer_tx
1535 .send(WriterCommand::SendOrReplay {
1536 key: MARKET_SUBSCRIPTION_REPLAY_KEY,
1537 data,
1538 })
1539 .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))
1540}
1541
1542fn reissue_order_subscription(
1543 request_id: &AtomicU64,
1544 active_id: &AtomicU64,
1545 lifecycle: &ProtocolLifecycle,
1546 sub_tx: &watch::Sender<Option<OrderSubscription>>,
1547 clk_tx: &watch::Sender<Option<String>>,
1548 initial_clk_tx: &watch::Sender<Option<String>>,
1549 writer_tx: Option<&WriterSender<WriterCommand>>,
1550) {
1551 let Some(writer_tx) = writer_tx else {
1552 log::error!("Cannot recover Betfair order stream before writer initialization");
1553 return;
1554 };
1555 let Some(mut sub) = sub_tx.borrow().clone() else {
1556 log::error!("Cannot recover Betfair order stream without a retained subscription");
1557 return;
1558 };
1559 let id = request_id.fetch_add(1, Ordering::Relaxed);
1560 sub.id = Some(id);
1561 sub.clk = None;
1562 sub.initial_clk = None;
1563 let data = match serde_json::to_vec(&sub) {
1564 Ok(data) => Bytes::from(data),
1565 Err(e) => {
1566 log::error!("Failed to serialize Betfair order recovery subscription: {e}");
1567 return;
1568 }
1569 };
1570
1571 active_id.store(id, Ordering::SeqCst);
1572 lifecycle.order.set(StreamLifecycleState::Pending);
1573 lifecycle.order_was_current.store(false, Ordering::Release);
1574 lifecycle
1575 .order_requires_image
1576 .store(true, Ordering::Release);
1577 lifecycle
1578 .order_image_tainted
1579 .store(false, Ordering::Release);
1580 let _ = clk_tx.send(None);
1581 let _ = initial_clk_tx.send(None);
1582 let _ = sub_tx.send(Some(sub));
1583
1584 if let Err(e) = writer_tx.send(WriterCommand::SendOrReplay {
1585 key: ORDER_SUBSCRIPTION_REPLAY_KEY,
1586 data,
1587 }) {
1588 log::error!("Failed to queue Betfair order recovery subscription: {e}");
1589 }
1590}
1591
1592fn update_stream_state(
1593 clk: &Option<String>,
1594 initial_clk: &Option<String>,
1595 heartbeat_ms: Option<u64>,
1596 clk_tx: &watch::Sender<Option<String>>,
1597 initial_clk_tx: &watch::Sender<Option<String>>,
1598 timeout_override: bool,
1599 dead_peer_timeout_ms: &AtomicU64,
1600) {
1601 if clk.is_some() {
1602 let _ = clk_tx.send(clk.clone());
1603 }
1604
1605 if initial_clk.is_some() {
1606 let _ = initial_clk_tx.send(initial_clk.clone());
1607 }
1608 update_negotiated_heartbeat(heartbeat_ms, timeout_override, dead_peer_timeout_ms);
1609}
1610
1611fn update_negotiated_heartbeat(
1612 interval_ms: Option<u64>,
1613 timeout_override: bool,
1614 dead_peer_timeout_ms: &AtomicU64,
1615) {
1616 if !timeout_override
1617 && let Some(interval_ms) = interval_ms
1618 && (BETFAIR_STREAM_HEARTBEAT_MIN_MS..=BETFAIR_STREAM_HEARTBEAT_MAX_MS)
1619 .contains(&interval_ms)
1620 {
1621 dead_peer_timeout_ms.store(interval_ms.saturating_mul(2), Ordering::Release);
1622 }
1623}
1624
1625fn outbound_heartbeat(interval_secs: Option<u64>) -> Option<SocketHeartbeat> {
1626 interval_secs.map(|interval_secs| SocketHeartbeat {
1627 interval_secs,
1628 payload: b"{\"op\":\"heartbeat\"}".to_vec(),
1629 })
1630}
1631
1632fn heartbeat_timeout(
1633 source: HeartbeatTimeoutSource,
1634 interval_secs: Option<u64>,
1635 timeout_secs: Option<u64>,
1636) -> Option<u64> {
1637 match source {
1638 HeartbeatTimeoutSource::Outbound => {
1639 interval_secs.map(|interval| timeout_secs.unwrap_or(interval.saturating_mul(2)))
1640 }
1641 HeartbeatTimeoutSource::Server => None,
1642 }
1643}
1644
1645fn validate_subscription_heartbeat(heartbeat_ms: u64) -> Result<(), BetfairStreamError> {
1646 if !(BETFAIR_STREAM_HEARTBEAT_MIN_MS..=BETFAIR_STREAM_HEARTBEAT_MAX_MS).contains(&heartbeat_ms)
1647 {
1648 return Err(BetfairStreamError::ProtocolError(format!(
1649 "subscription heartbeat must be in range [{BETFAIR_STREAM_HEARTBEAT_MIN_MS}, \
1650 {BETFAIR_STREAM_HEARTBEAT_MAX_MS}] ms, was {heartbeat_ms} ms",
1651 )));
1652 }
1653
1654 Ok(())
1655}
1656
1657struct AuxiliaryStreamSubscription {
1658 bytes: Bytes,
1659 label: &'static str,
1660 fatal_hint: &'static str,
1661 fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1662}
1663
1664impl AuxiliaryStreamSubscription {
1665 fn race(fatal_tx: tokio::sync::mpsc::UnboundedSender<()>) -> Result<Self, serde_json::Error> {
1666 Ok(Self {
1667 bytes: Bytes::from(serde_json::to_vec(&RaceSubscription::new(1))?),
1668 label: "race",
1669 fatal_hint: "check TPD entitlement on your Betfair app key",
1670 fatal_tx,
1671 })
1672 }
1673
1674 fn cricket(
1675 fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1676 ) -> Result<Self, serde_json::Error> {
1677 Ok(Self {
1678 bytes: Bytes::from(serde_json::to_vec(&CricketSubscription::new(1))?),
1679 label: "cricket",
1680 fatal_hint: "check cricket data entitlement on your Betfair app key",
1681 fatal_tx,
1682 })
1683 }
1684}
1685
1686#[derive(Clone)]
1687struct StreamAuth {
1688 generation: u64,
1689 bytes: Zeroizing<Vec<u8>>,
1690}
1691
1692impl Debug for StreamAuth {
1693 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1694 f.debug_struct(stringify!(StreamAuth))
1695 .field("generation", &self.generation)
1696 .field("bytes", &REDACTED)
1697 .finish()
1698 }
1699}
1700
1701#[derive(Debug, Default)]
1702struct ReconnectAuthState {
1703 replay_generation: AtomicU64,
1704 pending_generation: AtomicU64,
1705 reconnect_handle: OnceLock<SocketReconnectHandle>,
1706}
1707
1708impl ReconnectAuthState {
1709 fn set_handle(&self, handle: SocketReconnectHandle) {
1710 let result = self.reconnect_handle.set(handle);
1711 debug_assert!(result.is_ok(), "reconnect handle is set only once");
1712 }
1713
1714 fn record_replay(&self, generation: u64) {
1715 self.replay_generation.store(generation, Ordering::SeqCst);
1716 let _ = self
1717 .pending_generation
1718 .try_update(Ordering::SeqCst, Ordering::SeqCst, |pending| {
1719 (pending != 0 && pending <= generation).then_some(0)
1720 });
1721 }
1722
1723 fn request(&self, auth_generation: u64) -> ReconnectRequestOutcome {
1724 let Some(handle) = self.reconnect_handle.get() else {
1725 return ReconnectRequestOutcome::Unsupported;
1726 };
1727
1728 let outcome = handle.request_reconnect();
1729 if outcome == ReconnectRequestOutcome::AlreadyReconnecting
1730 && auth_generation > self.replay_generation.load(Ordering::SeqCst)
1731 {
1732 self.pending_generation
1733 .fetch_max(auth_generation, Ordering::SeqCst);
1734 }
1735
1736 outcome
1737 }
1738
1739 fn request_pending(&self) {
1740 let pending_generation = self.pending_generation.load(Ordering::SeqCst);
1741 if pending_generation == 0
1742 || pending_generation <= self.replay_generation.load(Ordering::SeqCst)
1743 {
1744 return;
1745 }
1746
1747 let Some(handle) = self.reconnect_handle.get() else {
1748 return;
1749 };
1750
1751 match handle.request_reconnect() {
1752 ReconnectRequestOutcome::Accepted => {
1753 let _ = self.pending_generation.compare_exchange(
1754 pending_generation,
1755 0,
1756 Ordering::SeqCst,
1757 Ordering::SeqCst,
1758 );
1759 }
1760 ReconnectRequestOutcome::AlreadyReconnecting => {}
1761 ReconnectRequestOutcome::Disconnected
1762 | ReconnectRequestOutcome::Closed
1763 | ReconnectRequestOutcome::Unsupported => {
1764 self.pending_generation.store(0, Ordering::SeqCst);
1765 }
1766 }
1767 }
1768}
1769
1770#[cfg(test)]
1771mod tests {
1772 use nautilus_network::SocketState;
1773 use rstest::rstest;
1774
1775 use super::*;
1776 use crate::stream::messages::{
1777 Authentication, CricketSubscription, MarketDataFilter, RaceSubscription, StreamMarketFilter,
1778 };
1779
1780 #[rstest]
1781 #[case::no_source(HeartbeatTimeoutSource::Outbound, None, None, None)]
1782 #[case::outbound_override(HeartbeatTimeoutSource::Outbound, Some(5), Some(60), Some(60))]
1783 #[case::outbound_derived(HeartbeatTimeoutSource::Outbound, Some(5), None, Some(10))]
1784 #[case::server(HeartbeatTimeoutSource::Server, None, None, None)]
1785 fn test_heartbeat_timeout(
1786 #[case] source: HeartbeatTimeoutSource,
1787 #[case] interval_secs: Option<u64>,
1788 #[case] timeout_secs: Option<u64>,
1789 #[case] expected: Option<u64>,
1790 ) {
1791 assert_eq!(
1792 heartbeat_timeout(source, interval_secs, timeout_secs),
1793 expected
1794 );
1795 }
1796
1797 #[rstest]
1798 fn test_reissue_before_writer_initialization_stays_fail_closed() {
1799 let request_id = AtomicU64::new(17);
1800 let market_active_id = AtomicU64::new(11);
1801 let order_active_id = AtomicU64::new(13);
1802 let lifecycle = ProtocolLifecycle::default();
1803 lifecycle.market.set(StreamLifecycleState::Degraded);
1804 lifecycle.order.set(StreamLifecycleState::Degraded);
1805 let (market_sub_tx, _market_sub_rx) = watch::channel(None::<MarketSubscription>);
1806 let (order_sub_tx, _order_sub_rx) = watch::channel(None::<OrderSubscription>);
1807 let (market_clk_tx, _market_clk_rx) = watch::channel(None::<String>);
1808 let (market_initial_clk_tx, _market_initial_clk_rx) = watch::channel(None::<String>);
1809 let (order_clk_tx, _order_clk_rx) = watch::channel(None::<String>);
1810 let (order_initial_clk_tx, _order_initial_clk_rx) = watch::channel(None::<String>);
1811
1812 let market_result = reissue_market_subscription(
1813 &request_id,
1814 &market_active_id,
1815 &lifecycle,
1816 &market_sub_tx,
1817 &market_clk_tx,
1818 &market_initial_clk_tx,
1819 None,
1820 );
1821 reissue_order_subscription(
1822 &request_id,
1823 &order_active_id,
1824 &lifecycle,
1825 &order_sub_tx,
1826 &order_clk_tx,
1827 &order_initial_clk_tx,
1828 None,
1829 );
1830
1831 assert!(matches!(
1832 market_result,
1833 Err(BetfairStreamError::Disconnected(_))
1834 ));
1835 assert_eq!(
1836 (
1837 request_id.load(Ordering::Acquire),
1838 market_active_id.load(Ordering::Acquire),
1839 order_active_id.load(Ordering::Acquire),
1840 lifecycle.market.get(),
1841 lifecycle.order.get(),
1842 ),
1843 (
1844 17,
1845 11,
1846 13,
1847 StreamLifecycleState::Degraded,
1848 StreamLifecycleState::Degraded,
1849 ),
1850 );
1851 }
1852
1853 #[rstest]
1854 fn test_invalid_clock_status_resets_clocks() {
1855 let (market_clk_tx, market_clk_rx) = watch::channel(Some("old-market-clk".to_string()));
1856 let (market_initial_clk_tx, market_initial_clk_rx) =
1857 watch::channel(Some("old-market-iclk".to_string()));
1858 let (order_clk_tx, order_clk_rx) = watch::channel(Some("old-order-clk".to_string()));
1859 let (order_initial_clk_tx, order_initial_clk_rx) =
1860 watch::channel(Some("old-order-iclk".to_string()));
1861
1862 let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1863 if let Ok(msg) = stream_decode(data)
1864 && let StreamMessage::Status(status) = &msg
1865 && status.error_code == Some(StatusErrorCode::InvalidClock)
1866 {
1867 let _ = market_clk_tx.send(None);
1868 let _ = market_initial_clk_tx.send(None);
1869 let _ = order_clk_tx.send(None);
1870 let _ = order_initial_clk_tx.send(None);
1871 }
1872 });
1873
1874 handler(
1875 br#"{"op":"status","statusCode":"503","errorCode":"INVALID_CLOCK","connectionClosed":true}"#,
1876 );
1877
1878 assert!(
1879 market_clk_rx.borrow().is_none(),
1880 "market clk must be cleared"
1881 );
1882 assert!(
1883 market_initial_clk_rx.borrow().is_none(),
1884 "market initialClk must be cleared"
1885 );
1886 assert!(order_clk_rx.borrow().is_none(), "order clk must be cleared");
1887 assert!(
1888 order_initial_clk_rx.borrow().is_none(),
1889 "order initialClk must be cleared"
1890 );
1891 }
1892
1893 #[rstest]
1894 fn test_auth_message_serialization() {
1895 let auth = Authentication::new("my-app-key".to_string(), "my-session".to_string());
1896 let json = serde_json::to_string(&auth).unwrap();
1897 assert!(json.contains("\"op\":\"authentication\""));
1898 assert!(json.contains("\"appKey\":\"my-app-key\""));
1899 assert!(json.contains("\"session\":\"my-session\""));
1900 }
1901
1902 #[rstest]
1903 #[case::exchange(true)]
1904 #[case::auxiliary(false)]
1905 fn test_update_auth_state_changes_once_per_distinct_payload(#[case] with_id: bool) {
1906 let make_auth = |session: &str| {
1907 if with_id {
1908 Authentication::with_id(
1909 "test-app-key".to_string(),
1910 session.to_string(),
1911 AUTH_REQUEST_ID,
1912 )
1913 } else {
1914 Authentication::new("test-app-key".to_string(), session.to_string())
1915 }
1916 };
1917 let initial = make_auth("initial");
1918 let initial_bytes = serde_json::to_vec(&initial).unwrap();
1919 let (auth_tx, auth_rx) = watch::channel(StreamAuth {
1920 generation: 7,
1921 bytes: Zeroizing::new(initial_bytes.clone()),
1922 });
1923
1924 update_auth_state(&auth_tx, &initial);
1925 let debug = format!("{:?}", auth_rx.borrow());
1926 assert_eq!(auth_rx.borrow().generation, 7);
1927 assert_eq!(auth_rx.borrow().bytes.as_slice(), initial_bytes);
1928 assert!(debug.contains(REDACTED));
1929 assert!(!debug.contains("initial"));
1930
1931 let replacement = make_auth("replacement");
1932 let replacement_bytes = serde_json::to_vec(&replacement).unwrap();
1933 update_auth_state(&auth_tx, &replacement);
1934 assert_eq!(auth_rx.borrow().generation, 8);
1935 assert_eq!(auth_rx.borrow().bytes.as_slice(), replacement_bytes);
1936
1937 update_auth_state(&auth_tx, &replacement);
1938 assert_eq!(auth_rx.borrow().generation, 8);
1939 }
1940
1941 #[rstest]
1942 fn test_clk_is_updated_from_mcm() {
1943 let (market_clk_tx, market_clk_rx) = watch::channel(None::<String>);
1944 let (market_initial_clk_tx, market_initial_clk_rx) = watch::channel(None::<String>);
1945 let (order_clk_tx, order_clk_rx) = watch::channel(None::<String>);
1946 let (order_initial_clk_tx, order_initial_clk_rx) = watch::channel(None::<String>);
1947 let market_active_sub_id = Arc::new(AtomicU64::new(5));
1948 let order_active_sub_id = Arc::new(AtomicU64::new(6));
1949
1950 let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1951 if let Ok(msg) = stream_decode(data) {
1952 match &msg {
1953 StreamMessage::MarketChange(mcm) => {
1954 let active = market_active_sub_id.load(Ordering::SeqCst);
1955 if active > 0 && mcm.id.is_none_or(|id| id == active) {
1956 if mcm.clk.is_some() {
1957 let _ = market_clk_tx.send(mcm.clk.clone());
1958 }
1959
1960 if mcm.initial_clk.is_some() {
1961 let _ = market_initial_clk_tx.send(mcm.initial_clk.clone());
1962 }
1963 }
1964 }
1965 StreamMessage::OrderChange(ocm) => {
1966 let active = order_active_sub_id.load(Ordering::SeqCst);
1967 if active > 0 && ocm.id.is_none_or(|id| id == active) {
1968 if ocm.clk.is_some() {
1969 let _ = order_clk_tx.send(ocm.clk.clone());
1970 }
1971
1972 if ocm.initial_clk.is_some() {
1973 let _ = order_initial_clk_tx.send(ocm.initial_clk.clone());
1974 }
1975 }
1976 }
1977 _ => {}
1978 }
1979 }
1980 });
1981
1982 handler(br#"{"op":"mcm","id":5,"pt":1000,"initialClk":"mcm-iclk","clk":"mcm-clk"}"#);
1984 handler(br#"{"op":"ocm","id":6,"pt":2000,"initialClk":"ocm-iclk","clk":"ocm-clk"}"#);
1985
1986 assert_eq!(market_clk_rx.borrow().as_deref(), Some("mcm-clk"));
1987 assert_eq!(market_initial_clk_rx.borrow().as_deref(), Some("mcm-iclk"));
1988 assert_eq!(order_clk_rx.borrow().as_deref(), Some("ocm-clk"));
1989 assert_eq!(order_initial_clk_rx.borrow().as_deref(), Some("ocm-iclk"));
1990
1991 handler(br#"{"op":"mcm","pt":1001,"clk":"hb-clk"}"#);
1993 assert_eq!(market_clk_rx.borrow().as_deref(), Some("hb-clk"));
1994
1995 handler(br#"{"op":"mcm","id":4,"pt":1002,"clk":"stale-clk"}"#);
1997 assert_eq!(market_clk_rx.borrow().as_deref(), Some("hb-clk"));
1998 }
1999
2000 #[rstest]
2001 fn test_reconnect_callback_sends_auth_and_subscription() {
2002 let (market_clk_tx, market_clk_rx) = watch::channel(Some("mcm-clk1".to_string()));
2003 let (market_initial_clk_tx, market_initial_clk_rx) =
2004 watch::channel(Some("mcm-iclk1".to_string()));
2005 let (order_clk_tx, order_clk_rx) = watch::channel(Some("ocm-clk1".to_string()));
2006 let (order_initial_clk_tx, order_initial_clk_rx) =
2007 watch::channel(Some("ocm-iclk1".to_string()));
2008 let (market_sub_tx, market_sub_rx) = watch::channel(None::<MarketSubscription>);
2009 let (order_sub_tx, order_sub_rx) = watch::channel(None::<OrderSubscription>);
2010
2011 let auth = Authentication::new("key".to_string(), "token".to_string());
2012 let auth_bytes = Bytes::from(serde_json::to_vec(&auth).unwrap());
2013
2014 let _ = market_sub_tx.send(Some(MarketSubscription {
2015 op: STREAM_OP_MARKET_SUBSCRIPTION.to_string(),
2016 id: Some(1),
2017 market_filter: StreamMarketFilter::default(),
2018 market_data_filter: MarketDataFilter::default(),
2019 clk: None,
2020 conflate_ms: None,
2021 heartbeat_ms: Some(BETFAIR_STREAM_HEARTBEAT_MAX_MS),
2022 initial_clk: None,
2023 segmentation_enabled: Some(true),
2024 }));
2025 let _ = order_sub_tx.send(Some(OrderSubscription {
2026 op: STREAM_OP_ORDER_SUBSCRIPTION.to_string(),
2027 id: Some(2),
2028 order_filter: None,
2029 clk: None,
2030 conflate_ms: None,
2031 heartbeat_ms: Some(BETFAIR_STREAM_HEARTBEAT_MAX_MS),
2032 initial_clk: None,
2033 segmentation_enabled: Some(true),
2034 }));
2035
2036 let auth_bytes_reconnect = auth_bytes;
2037 let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
2038 let mut replay = Vec::with_capacity(3);
2039 let market_sub = market_sub_rx.borrow().clone();
2040 let order_sub = order_sub_rx.borrow().clone();
2041
2042 replay.push(auth_bytes_reconnect.clone());
2043
2044 if let Some(mut sub) = market_sub {
2045 sub.clk = market_clk_rx.borrow().clone();
2046 sub.initial_clk = market_initial_clk_rx.borrow().clone();
2047 if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
2048 replay.push(Bytes::from(sub_bytes));
2049 }
2050 }
2051
2052 if let Some(mut sub) = order_sub {
2053 sub.clk = order_clk_rx.borrow().clone();
2054 sub.initial_clk = order_initial_clk_rx.borrow().clone();
2055 if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
2056 replay.push(Bytes::from(sub_bytes));
2057 }
2058 }
2059
2060 replay
2061 });
2062
2063 drop(market_clk_tx);
2064 drop(market_initial_clk_tx);
2065 drop(order_clk_tx);
2066 drop(order_initial_clk_tx);
2067
2068 let replay = reconnect_replay();
2069 let [auth_bytes, market_bytes, order_bytes] = replay.as_slice() else {
2070 panic!("expected auth, market, and order replay messages");
2071 };
2072
2073 let auth_str = std::str::from_utf8(auth_bytes).unwrap();
2074 let market_str = std::str::from_utf8(market_bytes).unwrap();
2075 let order_str = std::str::from_utf8(order_bytes).unwrap();
2076
2077 assert!(auth_str.contains("\"op\":\"authentication\""));
2078 assert!(market_str.contains("\"op\":\"marketSubscription\""));
2079 assert!(market_str.contains("\"clk\":\"mcm-clk1\""));
2081 assert!(market_str.contains("\"initialClk\":\"mcm-iclk1\""));
2082
2083 assert!(order_str.contains("\"op\":\"orderSubscription\""));
2084 assert!(order_str.contains("\"clk\":\"ocm-clk1\""));
2085 assert!(order_str.contains("\"initialClk\":\"ocm-iclk1\""));
2086 }
2087
2088 #[rstest]
2089 #[tokio::test]
2090 async fn test_auth_update_after_replay_snapshot_requests_follow_up() {
2091 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
2092
2093 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2094 let port = listener.local_addr().unwrap().port();
2095 let server = tokio::spawn(async move {
2096 let (socket, _) = listener.accept().await.unwrap();
2097 let (read_half, _write_half) = socket.into_split();
2098 let mut reader = BufReader::new(read_half);
2099 let mut line = String::new();
2100 reader.read_line(&mut line).await.unwrap();
2101 line.clear();
2102 reader.read_line(&mut line).await.unwrap();
2103
2104 let (socket, _) = listener.accept().await.unwrap();
2105 let (read_half, mut write_half) = socket.into_split();
2106 let mut reader = BufReader::new(read_half);
2107 line.clear();
2108 reader.read_line(&mut line).await.unwrap();
2109 let auth: serde_json::Value = serde_json::from_str(&line).unwrap();
2110 assert_eq!(auth["session"], "replacement-1");
2111 line.clear();
2112 reader.read_line(&mut line).await.unwrap();
2113 write_half
2114 .write_all(b"{\"op\":\"connection\",\"connectionId\":\"replacement-1\"}\r\n")
2115 .await
2116 .unwrap();
2117
2118 let (socket, _) = listener.accept().await.unwrap();
2119 let (read_half, _write_half) = socket.into_split();
2120 let mut reader = BufReader::new(read_half);
2121 line.clear();
2122 reader.read_line(&mut line).await.unwrap();
2123 let auth: serde_json::Value = serde_json::from_str(&line).unwrap();
2124 assert_eq!(auth["session"], "replacement-2");
2125 line.clear();
2126 reader.read_line(&mut line).await.unwrap();
2127 let subscription: serde_json::Value = serde_json::from_str(&line).unwrap();
2128 assert_eq!(subscription["op"], "orderSubscription");
2129 });
2130
2131 let credential = BetfairCredential::new(
2132 "testuser".to_string(),
2133 "testpass".to_string(),
2134 "test-app-key".to_string(),
2135 );
2136 let config = BetfairStreamConfig {
2137 host: "127.0.0.1".to_string(),
2138 port,
2139 heartbeat_secs: None,
2140 heartbeat_timeout_secs: Some(60),
2141 reconnect_delay_initial_ms: 200,
2142 reconnect_delay_max_ms: 1_000,
2143 use_tls: false,
2144 };
2145 let client = BetfairStreamClient::connect(
2146 &credential,
2147 "initial".to_string(),
2148 Arc::new(|_| {}),
2149 config,
2150 )
2151 .await
2152 .unwrap();
2153 client.subscribe_orders(None, Some(5_000)).await.unwrap();
2154
2155 client.update_auth("test-app-key", "replacement-1".to_string());
2156 assert!(client.request_reconnect());
2157 tokio::time::timeout(std::time::Duration::from_secs(2), async {
2158 while client
2159 .reconnect_auth
2160 .replay_generation
2161 .load(Ordering::SeqCst)
2162 < 1
2163 {
2164 tokio::task::yield_now().await;
2165 }
2166 })
2167 .await
2168 .unwrap();
2169
2170 client.update_auth("test-app-key", "replacement-2".to_string());
2171 assert!(!client.request_reconnect());
2172 assert_eq!(
2173 client
2174 .reconnect_auth
2175 .pending_generation
2176 .load(Ordering::SeqCst),
2177 2,
2178 );
2179
2180 tokio::time::timeout(std::time::Duration::from_secs(5), server)
2181 .await
2182 .unwrap()
2183 .unwrap();
2184 client.close().await.expect("close stream");
2185 }
2186
2187 #[rstest]
2188 #[case::no_subscription(false, "Protocol error: no retained market subscription")]
2189 #[case::closed(true, "Disconnected: stream client is closed")]
2190 #[tokio::test]
2191 async fn test_request_market_image_fails_without_open_subscription(
2192 #[case] subscribe_then_close: bool,
2193 #[case] expected: &str,
2194 ) {
2195 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2196 let port = listener.local_addr().unwrap().port();
2197
2198 let server = tokio::spawn(async move {
2199 let (socket, _) = listener.accept().await.unwrap();
2200 let mut lines = tokio::io::AsyncBufReadExt::lines(tokio::io::BufReader::new(socket));
2201 while let Ok(Some(_)) = lines.next_line().await {}
2202 });
2203
2204 let credential = BetfairCredential::new(
2205 "testuser".to_string(),
2206 "testpass".to_string(),
2207 "test-app-key".to_string(),
2208 );
2209
2210 let config = BetfairStreamConfig {
2211 host: "127.0.0.1".to_string(),
2212 port,
2213 heartbeat_secs: None,
2214 heartbeat_timeout_secs: Some(60),
2215 reconnect_delay_initial_ms: 200,
2216 reconnect_delay_max_ms: 1_000,
2217 use_tls: false,
2218 };
2219
2220 let client = BetfairStreamClient::connect(
2221 &credential,
2222 "session".to_string(),
2223 Arc::new(|_| {}),
2224 config,
2225 )
2226 .await
2227 .unwrap();
2228
2229 if subscribe_then_close {
2230 client
2231 .subscribe_markets(Default::default(), Default::default(), None, None)
2232 .await
2233 .unwrap();
2234 client.close().await.expect("close stream");
2235 }
2236
2237 let result = client.request_market_image(&SnapshotGate::default());
2238
2239 assert_eq!(result.unwrap_err().to_string(), expected);
2240 client.close().await.expect("close stream");
2241 server.abort();
2242 }
2243
2244 #[rstest]
2245 fn test_race_subscription_serialization() {
2246 let sub = RaceSubscription::new(42);
2247 let json = serde_json::to_string(&sub).unwrap();
2248 assert!(json.contains("\"op\":\"raceSubscription\""));
2249 assert!(json.contains("\"id\":42"));
2250 }
2251
2252 #[rstest]
2253 fn test_cricket_subscription_serialization() {
2254 let sub = CricketSubscription::new(42);
2255 let json = serde_json::to_string(&sub).unwrap();
2256 assert!(json.contains("\"op\":\"cricketSubscription\""));
2257 assert!(json.contains("\"id\":42"));
2258 }
2259
2260 #[rstest]
2261 #[case::race(false, "raceSubscription")]
2262 #[case::cricket(true, "cricketSubscription")]
2263 #[tokio::test]
2264 async fn test_auxiliary_stream_state_and_controller_reconnect(
2265 #[case] cricket: bool,
2266 #[case] subscription_op: &'static str,
2267 ) {
2268 use std::time::Duration;
2269
2270 use parking_lot::Mutex;
2271 use tokio::io::{AsyncBufReadExt, BufReader};
2272
2273 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2274 let port = listener.local_addr().unwrap().port();
2275 let (initial_tx, initial_rx) = tokio::sync::oneshot::channel();
2276 let (replacement_tx, replacement_rx) = tokio::sync::oneshot::channel();
2277 let (done_tx, done_rx) = tokio::sync::oneshot::channel();
2278
2279 let server = tokio::spawn(async move {
2280 let (socket, _) = listener.accept().await.unwrap();
2281 let (read_half, initial_write_half) = socket.into_split();
2282 let mut initial_reader = BufReader::new(read_half);
2283 let mut auth = String::new();
2284 let mut subscription = String::new();
2285 initial_reader.read_line(&mut auth).await.unwrap();
2286 initial_reader.read_line(&mut subscription).await.unwrap();
2287 let auth: serde_json::Value = serde_json::from_str(&auth).unwrap();
2288 let subscription: serde_json::Value = serde_json::from_str(&subscription).unwrap();
2289 assert_eq!(auth["session"], "test-session");
2290 assert_eq!(subscription["op"], subscription_op);
2291 initial_tx.send(()).unwrap();
2292
2293 let (socket, _) = listener.accept().await.unwrap();
2294 let (read_half, replacement_write_half) = socket.into_split();
2295 let mut replacement_reader = BufReader::new(read_half);
2296 let mut replay_auth = String::new();
2297 let mut replay_subscription = String::new();
2298 replacement_reader
2299 .read_line(&mut replay_auth)
2300 .await
2301 .unwrap();
2302 replacement_reader
2303 .read_line(&mut replay_subscription)
2304 .await
2305 .unwrap();
2306 let replay_auth: serde_json::Value = serde_json::from_str(&replay_auth).unwrap();
2307 let replay_subscription: serde_json::Value =
2308 serde_json::from_str(&replay_subscription).unwrap();
2309 assert_eq!(replay_auth, auth);
2310 assert_eq!(replay_subscription, subscription);
2311 replacement_tx.send(()).unwrap();
2312
2313 let _initial_connection = (initial_reader, initial_write_half);
2314 let _replacement_connection = (replacement_reader, replacement_write_half);
2315 let _ = done_rx.await;
2316 });
2317
2318 let states = Arc::new(Mutex::new(Vec::new()));
2319 let states_sink = Arc::clone(&states);
2320 let state_sink = SocketStateSink::new(move |state| {
2321 states_sink.lock().push(state);
2322 });
2323 let credential = BetfairCredential::new(
2324 "testuser".to_string(),
2325 "testpass".to_string(),
2326 "test-app-key".to_string(),
2327 );
2328 let config = BetfairStreamConfig {
2329 host: "127.0.0.1".to_string(),
2330 port,
2331 heartbeat_secs: Some(5),
2332 heartbeat_timeout_secs: Some(60),
2333 reconnect_delay_initial_ms: 100,
2334 reconnect_delay_max_ms: 500,
2335 use_tls: false,
2336 };
2337 let (fatal_tx, _fatal_rx) = tokio::sync::mpsc::unbounded_channel();
2338 let client = if cricket {
2339 BetfairRaceStreamClient::connect_cricket_decoded(
2340 &credential,
2341 "test-session".into(),
2342 Arc::new(|_| {}),
2343 config,
2344 fatal_tx,
2345 Some(state_sink),
2346 )
2347 .await
2348 .unwrap()
2349 } else {
2350 BetfairRaceStreamClient::connect_decoded(
2351 &credential,
2352 "test-session".into(),
2353 Arc::new(|_| {}),
2354 config,
2355 fatal_tx,
2356 Some(state_sink),
2357 )
2358 .await
2359 .unwrap()
2360 };
2361
2362 initial_rx.await.unwrap();
2363 assert_eq!(
2364 client.request_reconnect_outcome(),
2365 ReconnectRequestOutcome::Accepted,
2366 );
2367 tokio::time::timeout(Duration::from_secs(5), replacement_rx)
2368 .await
2369 .unwrap()
2370 .unwrap();
2371 tokio::time::timeout(Duration::from_secs(5), async {
2372 while states.lock().len() < 3 {
2373 tokio::task::yield_now().await;
2374 }
2375 })
2376 .await
2377 .unwrap();
2378 client.close().await;
2379
2380 assert_eq!(
2381 *states.lock(),
2382 vec![
2383 SocketState::Connected,
2384 SocketState::Disconnected,
2385 SocketState::Connected,
2386 ],
2387 );
2388 assert_eq!(
2389 client.request_reconnect_outcome(),
2390 ReconnectRequestOutcome::Closed,
2391 );
2392
2393 let _ = done_tx.send(());
2394 server.await.unwrap();
2395 }
2396
2397 #[rstest]
2398 fn test_race_stream_reconnect_replays_auth_and_subscription() {
2399 let auth = Authentication::new("key".to_string(), "token".to_string());
2400 let auth_bytes = Bytes::from(serde_json::to_vec(&auth).unwrap());
2401 let race_sub = RaceSubscription::new(1);
2402 let race_sub_bytes = Bytes::from(serde_json::to_vec(&race_sub).unwrap());
2403
2404 let auth_reconnect = auth_bytes;
2405 let sub_reconnect = race_sub_bytes;
2406 let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
2407 let mut combined = Vec::with_capacity(auth_reconnect.len() + 2 + sub_reconnect.len());
2408 combined.extend_from_slice(&auth_reconnect);
2409 combined.extend_from_slice(b"\r\n");
2410 combined.extend_from_slice(&sub_reconnect);
2411 vec![Bytes::from(combined)]
2412 });
2413
2414 let replay = reconnect_replay();
2415 let [bytes] = replay.as_slice() else {
2416 panic!("expected one combined replay message");
2417 };
2418
2419 let text = std::str::from_utf8(bytes).unwrap();
2420 let (auth_part, sub_part) = text
2421 .split_once("\r\n")
2422 .expect("CRLF separator in combined message");
2423
2424 assert!(auth_part.contains("\"op\":\"authentication\""));
2425 assert!(sub_part.contains("\"op\":\"raceSubscription\""));
2426 }
2427
2428 #[rstest]
2429 fn test_race_stream_handler_fatal_status_sends_kill_signal() {
2430 let (race_fatal_tx, mut race_fatal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
2431 let inner_handler: TcpMessageHandler = Arc::new(|_data: &[u8]| {});
2432
2433 let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
2434 if let Ok(StreamMessage::Status(status)) = stream_decode(data)
2435 && let Some(ref code) = status.error_code
2436 && code.is_race_stream_fatal()
2437 {
2438 let _ = race_fatal_tx.send(());
2439 return;
2440 }
2441 inner_handler(data);
2442 });
2443
2444 handler(
2446 br#"{"op":"status","statusCode":"503","errorCode":"NOT_AUTHORIZED","connectionClosed":true}"#,
2447 );
2448 assert!(
2449 race_fatal_rx.try_recv().is_ok(),
2450 "fatal error must send kill signal"
2451 );
2452
2453 handler(
2455 br#"{"op":"status","statusCode":"503","errorCode":"INVALID_CLOCK","connectionClosed":true}"#,
2456 );
2457 assert!(
2458 race_fatal_rx.try_recv().is_err(),
2459 "non-fatal error must not send kill signal"
2460 );
2461 }
2462}