use std::sync::Arc;
use tokio::sync::mpsc;
use tracing::debug;
use super::event::GapEvent;
pub struct TradeIdGapDetector {
symbol: Arc<str>,
last_known_tid: Option<i64>,
gap_event_tx: Option<mpsc::Sender<GapEvent>>,
gaps_detected: u64,
}
impl TradeIdGapDetector {
pub fn new(symbol: Arc<str>, gap_event_tx: Option<mpsc::Sender<GapEvent>>) -> Self {
Self {
symbol,
last_known_tid: None,
gap_event_tx,
gaps_detected: 0,
}
}
pub fn seed(&mut self, last_tid: i64) {
self.last_known_tid = Some(last_tid);
}
pub fn check(&mut self, incoming_tid: i64) -> Option<(i64, i64)> {
let last_id = self.last_known_tid?;
if incoming_tid <= last_id + 1 {
return None;
}
let gap_size = incoming_tid - last_id - 1;
self.gaps_detected += 1;
if let Some(ref tx) = self.gap_event_tx {
let event = GapEvent {
symbol: self.symbol.clone(),
expected_tid: last_id + 1,
actual_tid: incoming_tid,
gap_size,
detected_at_ms: chrono::Utc::now().timestamp_millis(),
filled_inline: false, };
if tx.try_send(event).is_err() {
debug!(
symbol = %self.symbol,
gap_size,
"gap event channel full, event dropped"
);
}
}
Some((last_id, gap_size))
}
pub fn update_last_tid(&mut self, tid: i64) {
self.last_known_tid = Some(tid);
}
pub fn mark_filled(&self, last_known_id: i64, gap_size: i64) {
if let Some(ref tx) = self.gap_event_tx {
let event = GapEvent {
symbol: self.symbol.clone(),
expected_tid: last_known_id + 1,
actual_tid: last_known_id + gap_size + 1,
gap_size,
detected_at_ms: chrono::Utc::now().timestamp_millis(),
filled_inline: true,
};
let _ = tx.try_send(event);
}
}
pub fn gaps_detected(&self) -> u64 {
self.gaps_detected
}
pub fn last_known_tid(&self) -> Option<i64> {
self.last_known_tid
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::mpsc;
#[test]
fn test_first_trade_no_gap() {
let (tx, _rx) = mpsc::channel(16);
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
assert!(detector.check(100).is_none());
}
#[test]
fn test_contiguous_trades_no_gap() {
let (tx, _rx) = mpsc::channel(16);
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
detector.update_last_tid(100);
assert!(detector.check(101).is_none());
}
#[test]
fn test_gap_detected() {
let (tx, mut rx) = mpsc::channel(16);
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
detector.update_last_tid(100);
let result = detector.check(105);
assert!(result.is_some());
let (last_id, gap_size) = result.unwrap();
assert_eq!(last_id, 100);
assert_eq!(gap_size, 4);
assert_eq!(detector.gaps_detected(), 1);
let event = rx.try_recv().unwrap();
assert_eq!(&*event.symbol, "BTCUSDT");
assert_eq!(event.expected_tid, 101);
assert_eq!(event.actual_tid, 105);
assert_eq!(event.gap_size, 4);
assert!(!event.filled_inline);
}
#[test]
fn test_seeded_gap_detection() {
let (tx, mut rx) = mpsc::channel(16);
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
detector.seed(1000);
let result = detector.check(1010);
assert!(result.is_some());
let (last_id, gap_size) = result.unwrap();
assert_eq!(last_id, 1000);
assert_eq!(gap_size, 9);
let event = rx.try_recv().unwrap();
assert_eq!(event.expected_tid, 1001);
}
#[test]
fn test_mark_filled() {
let (tx, mut rx) = mpsc::channel(16);
let detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
detector.mark_filled(100, 5);
let event = rx.try_recv().unwrap();
assert!(event.filled_inline);
assert_eq!(event.gap_size, 5);
}
#[test]
fn test_no_channel_still_detects() {
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), None);
detector.update_last_tid(100);
let result = detector.check(105);
assert!(result.is_some());
assert_eq!(detector.gaps_detected(), 1);
}
#[test]
fn test_multi_gap_counting() {
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), None);
detector.update_last_tid(100);
assert!(detector.check(110).is_some());
detector.update_last_tid(110);
assert!(detector.check(120).is_some());
detector.update_last_tid(120);
assert_eq!(detector.gaps_detected(), 2);
}
#[test]
fn test_overlap_not_gap() {
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), None);
detector.update_last_tid(100);
assert!(detector.check(90).is_none());
assert_eq!(detector.gaps_detected(), 0);
}
#[test]
fn test_single_trade_gap() {
let (tx, mut rx) = mpsc::channel(16);
let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));
detector.update_last_tid(100);
let result = detector.check(102);
assert!(result.is_some());
let (_, gap_size) = result.unwrap();
assert_eq!(gap_size, 1);
let event = rx.try_recv().unwrap();
assert_eq!(event.gap_size, 1);
}
}