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