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