use std::time::Duration;
use nautilus_common::messages::DataEvent;
use nautilus_core::UUID4;
pub async fn drain_data_events(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
timeout: Duration,
) -> Vec<DataEvent> {
let mut events = Vec::new();
let deadline = tokio::time::Instant::now() + timeout;
while let Ok(Some(event)) = tokio::time::timeout_at(deadline, rx.recv()).await {
events.push(event);
}
events
}
pub async fn collect_data_events_until_response(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
request_id: UUID4,
timeout: Duration,
) -> Vec<DataEvent> {
let mut events = Vec::new();
tokio::time::timeout(timeout, async {
loop {
let event = rx.recv().await.expect("data event channel closed");
let is_correlated_response = matches!(
&event,
DataEvent::Response(response) if response.correlation_id() == &request_id
);
events.push(event);
if is_correlated_response {
break;
}
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for data response {request_id}"));
while let Ok(event) = rx.try_recv() {
events.push(event);
}
events
}
#[cfg(test)]
mod tests {
use nautilus_common::messages::{DataResponse, data::InstrumentsResponse};
use nautilus_core::UnixNanos;
use nautilus_model::{
identifiers::{ClientId, Venue},
instruments::{InstrumentAny, stubs::equity_aapl_itch},
stubs::TestDefault,
};
use rstest::rstest;
use super::*;
fn instruments_response(correlation_id: UUID4) -> DataEvent {
DataEvent::Response(DataResponse::Instruments(InstrumentsResponse::new(
correlation_id,
ClientId::test_default(),
Venue::test_default(),
Vec::new(),
None,
None,
UnixNanos::default(),
None,
)))
}
fn correlation_ids(events: &[DataEvent]) -> Vec<Option<UUID4>> {
events
.iter()
.map(|event| match event {
DataEvent::Response(response) => Some(*response.correlation_id()),
_ => None,
})
.collect()
}
#[rstest]
#[case(0)]
#[case(3)]
#[tokio::test(start_paused = true)]
async fn test_drain_data_events_collects_events_queued_before_deadline(#[case] count: usize) {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let request_ids: Vec<UUID4> = (0..count).map(|_| UUID4::new()).collect();
for request_id in &request_ids {
tx.send(instruments_response(*request_id)).unwrap();
}
let events = drain_data_events(&mut rx, Duration::from_millis(50)).await;
assert_eq!(
correlation_ids(&events),
request_ids.iter().copied().map(Some).collect::<Vec<_>>()
);
}
#[rstest]
#[tokio::test(start_paused = true)]
async fn test_drain_data_events_stops_at_the_absolute_deadline() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let first_id = UUID4::new();
let second_id = UUID4::new();
let late_id = UUID4::new();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(40)).await;
tx.send(instruments_response(first_id)).unwrap();
tokio::time::sleep(Duration::from_millis(40)).await;
tx.send(instruments_response(second_id)).unwrap();
tokio::time::sleep(Duration::from_millis(80)).await;
tx.send(instruments_response(late_id)).unwrap();
});
let start = tokio::time::Instant::now();
let events = drain_data_events(&mut rx, Duration::from_millis(100)).await;
assert_eq!(
correlation_ids(&events),
vec![Some(first_id), Some(second_id)]
);
assert_eq!(start.elapsed(), Duration::from_millis(100));
}
#[rstest]
#[tokio::test(start_paused = true)]
async fn test_drain_data_events_returns_when_channel_closes() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let request_id = UUID4::new();
tx.send(instruments_response(request_id)).unwrap();
drop(tx);
let start = tokio::time::Instant::now();
let events = drain_data_events(&mut rx, Duration::from_secs(5)).await;
assert_eq!(correlation_ids(&events), vec![Some(request_id)]);
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[rstest]
#[tokio::test(start_paused = true)]
async fn test_collect_data_events_until_response_returns_at_correlated_response() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let other_id = UUID4::new();
let request_id = UUID4::new();
let trailing_id = UUID4::new();
tx.send(DataEvent::Instrument(InstrumentAny::Equity(
equity_aapl_itch(),
)))
.unwrap();
tx.send(instruments_response(other_id)).unwrap();
tx.send(instruments_response(request_id)).unwrap();
tx.send(instruments_response(trailing_id)).unwrap();
let start = tokio::time::Instant::now();
let events =
collect_data_events_until_response(&mut rx, request_id, Duration::from_secs(5)).await;
assert_eq!(
correlation_ids(&events),
vec![None, Some(other_id), Some(request_id), Some(trailing_id)]
);
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[rstest]
#[tokio::test(start_paused = true)]
#[should_panic(expected = "timed out waiting for data response")]
async fn test_collect_data_events_until_response_panics_without_correlated_response() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
tx.send(instruments_response(UUID4::new())).unwrap();
collect_data_events_until_response(&mut rx, UUID4::new(), Duration::from_millis(50)).await;
}
#[rstest]
#[tokio::test]
#[should_panic(expected = "data event channel closed")]
async fn test_collect_data_events_until_response_panics_when_channel_closes() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
drop(tx);
collect_data_events_until_response(&mut rx, UUID4::new(), Duration::from_secs(5)).await;
}
}